「canvasでライフゲーム」にボタンを追加する

前のcanvasでライフゲームでは単純にライフゲームを動かしてみました。
今回はボタンの追加や、スピードの変更といった機能を追加してみます。

デモ

今回の完成形はこんな感じ。

See the Pen Untitled by hukinotou (@webspace) on CodePen.

コーディング

HTML

前回同様のcanvasタグに加えて、速度や操作用のボタンなどを追加します。

HTML
<div style="display: flex;">
  <div>
    <canvas id="canvas" width="400" height="400" style="border: solid 1px #999;"></canvas>
  </div>
  <div style="padding-left: .5em;">
    <p>
      <input type="range" id="speed" min="1" max="40">
      <span id="speed_txt"></span>
    </p>
    <p>
      <input type="button" id="reset" value="リセット">
      <input type="button" id="start" value="スタート">
      <input type="button" id="stop" value="停止">
      <input type="button" id="step" value="ステップ実行">
    </p>
    <p>
      生存数:<span id="active"></span>
    </p>
    <p>
      世代数:<span id="generation"></span>
    </p>
  </div>
</div>

Javascript

Javascript
jQuery(function($) {
    var WIDTH = 80;
    var HEIGHT = WIDTH;
    var SIZE = 5;

    var timer = null;
    var field;
    var canvas;
    var generation;
    var speed;

    var Generation = function(){
        this.$e = $("#generation");
        this.g = 0;
        this.show();
    };
    Generation.prototype = {
        increment: function(){
            this.g++;
        },
        reset: function(){
            this.g = 0;
        },
        show: function() {
            this.$e.text(this.g);
        }
    };

    var Speed = function(){
        this.$e = $("#speed");
        this.s = this.val();
        this.show();
    };
    Speed.prototype = {
        change: function(){
            var s = this.val();
            if (this.s === s) {
                return;
            }
            this.s = s;
            this.show();

            if (timer === null) {
                return;
            }
            start();
        },
        show: function(){
            $("#speed_txt").text(this.speed() + "ms");
        },
        val: function(){
            return this.$e.val();
        },
        speed: function(){
            return 25 * this.val();
        }
    };


    var Field = function(width, height){
        this.width = width;
        this.height = height;
        this.field;
        this.active;
        this.init();
    };
    Field.prototype = {
        init: function(){
            this.active = 0;
            this.field = [];
            for (var y = 0; y < this.height; y++) {
                this.field[y] = [];
                for (var x = 0; x < this.width; x++) {
                    this.set(x, y, null);
                }
            }
        },
        randomize: function(){
            for (var y = 0; y < this.height; y++) {
                for (var x = 0; x < this.width; x++) {
                    var val = random(1, 10) == 1 ? 1 : 0;
                    this.set(x, y, val);
                    this.active += this.val(x, y);
                }
            }
        },
        reset: function(){
            this.active = 0;
            for (var y = 0; y < this.height; y++) {
                for (var x = 0; x < this.width; x++) {
                    this.set(x, y, 0);
                }
            }
        },
        next: function(){
            this.active = 0;
            var tempField = $.extend(true, {}, this.field);
            for (var y = 0; y < this.height; y++) {
                for (var x = 0; x < this.width; x++) {
                    var n = this.neighbor(x, y, tempField);
                    var val = this.fate(tempField[y][x], n);
                    this.set(x, y, val);
                    this.active += this.val(x, y);
                }
            }
        },
        neighbor: function(x, y, field) {
            var n = 0;
            for (var s = -1; s < 2; s++) {
                if (y + s < 0 || y + s > this.height - 1) {
                    continue;
                }
                for (var t = -1; t < 2; t++) {
                    if (s == 0 && t == 0) {
                        continue;
                    }
                    if (x + t < 0 || x + t > this.width - 1) {
                        continue;
                    }
                    if (field[y + s][x + t] == 1) {
                        n++;
                    }
                }
            }
            return n;
        },
        fate: function(mine, neighbor){
            // 生存
            if (mine == 1 && (neighbor == 2 || neighbor == 3)) {
                return 1;
            }
            // 誕生
            if (mine == 0 && neighbor == 3) {
                return 1;
            }
            // 過疎, 過密
            return 0;
        },
        val: function(x, y){
            return this.field[y][x];
        },
        set: function(x, y, v){
            this.field[y][x] = v;
        }
    };

    var Canvas = function(ctx, field, size) {
        this.ctx = ctx;
        this.field = field;
        this.size = size;
    };
    Canvas.prototype = {
        clear: function(){
            this.ctx.clearRect(0, 0, this.field.width * this.size, this.field.height * this.size);
        },
        draw: function() {
            this.clear();
            this.ctx.fillStyle = 'rgb(0, 0, 0)';
            for (var y = 0; y < this.field.height; y++) {
                for (var x = 0; x < this.field.width; x++) {
                    if (this.field.val(x, y) == 0) {
                        continue;
                    }
                    this.ctx.fillRect(x * this.size, y * this.size, this.size, this.size);
                }
            }
        }
    };

    var init = function() {
        var $canvas = $("#canvas");
        var ctx = $canvas.get(0).getContext('2d');

        field = new Field(WIDTH, HEIGHT);
        field.randomize();

        canvas = new Canvas(ctx, field, SIZE);
        generation = new Generation();
        speed = new Speed();

        bind();
    };

    var bind = function() {
        var $e = $("#speed");
        $e.mousemove(function(){
            speed.change();
        });
        $e.change(function(){
            speed.change();
        });

        $("#reset").click(reset);
        $("#start").click(start);
        $("#stop").click(stop);
        $("#step").click(step);
    };

    var draw = function() {
        canvas.draw();
        generation.show();
        $("#active").text(field.active);
    };

    var lifegame = function() {
        generation.increment();
        field.next();
        draw();
    }

    var reset = function() {
        generation.reset();
        field.randomize();
        draw();
    };

    var start = function(){
        $("#start").prop("disabled", true);
        $("#stop").prop("disabled", false);
        $("#step").prop("disabled", true);

        if (timer !== null) {
            clearInterval(timer);
        }
        timer = setInterval(lifegame, speed.speed());
    };

    var stop = function() {
        $("#start").prop("disabled", false);
        $("#stop").prop("disabled", true);
        $("#step").prop("disabled", false);

        clearInterval(timer);
        timer = null;
    };

    var step = function() {
        if (timer !== null) {
            return;
        }
        lifegame();
    };

    var random = function(min, max) {
        return min + Math.floor(Math.random() * (max + 1));
    };

    init();
    draw();
});

簡単な説明

Javascriptの簡単な説明をつけておきます。

変数について

timergenerationspeedの3つを追加しました。

Javascript
var WIDTH = 80; // 横のセル数
var HEIGHT = WIDTH; // 縦のセル数
var SIZE = 5; // セルサイズ

var timer = null; // setInterval用
var field; // 各セルの状態管理用
var canvas; // canvas制御用
var generation; // 世代数管理用
var speed; // 表示スピード管理用

メソッドについて

追加したメソッドは5つ。

  • bind:各ボタンなどの部品へのイベントのバインド
  • reset:fieldを初期化
  • start:ライフゲームを開始する
  • stop:ライフゲームを停止する
  • step:ライフゲームをステップ実行する

Generationオブジェクト

メンバ変数

オブジェクトのメンバ変数は2つ。

Javascript
this.$e = $("#generation"); // 世代表示部のエレメント
this.g = 0; // 世代数

prototype(内部メソッド)

prototype(内部メソッド)として3つ。

  • increment:世代を1増やす
  • reset:世代数を0に戻す
  • show:世代数を表示する

Speed

メンバ変数

オブジェクトのメンバ変数は3つ。

Javascript
this.$e = $("#speed"); // 速度のスライドバーのエレメント
this.s = this.val(); // 速度の値

prototype(内部メソッド)

prototype(内部メソッド)として2つ。

  • change:スライドを変更したときの処理
  • show:速度を表示する
  • val:速度の値を取得する
  • speed:速度を計算する

おまけとして、現在activeなセルの数も表示するようにしてみました。