tmlib.js でグラフィックスプログラミング – 幾何学っぽいなにか

phi phi on tmlib.js

思いついたので, tmlib.js で幾何学っぽいやつ作りました. ちょっとしたグラフィックスプログラミングです.

下にデモとコードの解説載せてます.

ゲームプログラミングは, 乱数やベクトル, 行列を手足のように使えてナンボの世界なので こういったプログラミングに慣れとくと楽しいですよ♪

tmlib.js

Demo

とりあえず作ったやつです. パーティクル同士が近づくとラインを結びます. マウスにも反応します.

[runstant]

runstant で作ったので実際に実行したり, 上の CONFIG イジって見た目を変えたりできます.
よかったら遊んでみてください♪

Code

コードはこんな感じです.

/*
 * # tmlib.js でグラフィックスプログラミング - 幾何学っぽいなにか
 */

var SCREEN_WIDTH    = 465;              // スクリーン幅  
var SCREEN_HEIGHT   = 465;              // スクリーン高さ  
var SCREEN_CENTER_X = SCREEN_WIDTH/2;   // スクリーン幅の半分  
var SCREEN_CENTER_Y = SCREEN_HEIGHT/2;  // スクリーン高さの半分

// コンフィグ
var CONFIG = {  
    particles: 64,              // パーティクル数
    speed: 2,                   // スピード
    particleColor: "white",     // パーティクルの色
    lineColor: "white",         // ラインの色
    shadowColor: "white",       // 影の色
    backgroundColor: "black",   // 背景の色
    distance: 100,              // ラインを結ぶ判定距離
};


// main
tm.main(function() {  
    // キャンバスアプリケーションを生成
    var app = tm.display.CanvasApp("#world");
    // リサイズ
    app.resize(SCREEN_WIDTH, SCREEN_HEIGHT);
    // ウィンドウにフィットさせる
    app.fitWindow();
    // 背景色を指定
    app.background = CONFIG.backgroundColor;

    // シーン切り替え
    app.replaceScene(MainScene());

    // 実行
    app.run();

    // app.enableStats();
});

// シーンを定義
tm.define("MainScene", {  
    superClass: "tm.app.Scene",

    init: function() {
        this.superInit();

        this.fromJSON({
            children: {
                particleGroup: {
                    type: "tm.display.CanvasElement",
                },
            },
        });

        // パーティクルを生成
        (CONFIG.particles).times(function() {
            var p = Particle().addChildTo(this.particleGroup);
            p.setPosition(Math.rand(0, SCREEN_WIDTH), Math.rand(0, SCREEN_HEIGHT));
        }, this);

        // グループの描画を上書き
        this.particleGroup.draw = this.drawLines.bind(this);
    },

    drawLines: function(canvas) {
        var p = this.app.pointing;
        var particles = this.particleGroup.children;

        // ライン描画
        particles.each(function(particle, i) {
            // ポインタの位置との距離判定&ライン描画
            this.drawLine(canvas, p, particle);

            // 自分以外のパーティクルとの距離判定&ライン描画
            (particles.length-i-1).times(function(j) {
                var other = particles[j+i+1];
                this.drawLine(canvas, particle, other);
            }, this);
        }, this);
    },

    // ライン描画
    drawLine: function(canvas, a, b) {
        // 距離を取得
        var distance = tm.geom.Vector2.distance(a, b);

        // 距離判定
        if (distance < CONFIG.distance) {
            // 透明度を調整
            var alpha = (CONFIG.distance-distance)*0.01;
            canvas.strokeStyle = CONFIG.lineColor;
            canvas.globalAlpha = alpha;
            canvas.blendMode = "lighter";
            // 影を設定
            canvas.shadowColor = CONFIG.shadowColor;
            canvas.shadowBlur = 4;
            // 描画
            canvas.drawLine(a.x, a.y, b.x, b.y);
        }
    },
});

// パーティクルクラス
tm.define("Particle", {  
    superClass: "tm.display.CircleShape",

    init: function() {
        this.superInit({
            width: 4,
            height: 4,
            strokeStyle: "transparent",
            fillStyle: CONFIG.particleColor,
        });

        // ランダムな方向へのベクトル
        this.v = tm.geom.Vector2.random(0, 360, CONFIG.speed);
    },

    update: function() {
        // 移動
        this.position.add(this.v);

        // 画面から出ないよう制御
        if (this.x < 0) { this.x = 0; this.v.x *= -1; }
        else if (this.x > SCREEN_WIDTH) { this.x = SCREEN_WIDTH; this.v.x *= -1; }
        if (this.y < 0) { this.y = 0; this.v.y *= -1; }
        else if (this.y > SCREEN_HEIGHT) { this.y = SCREEN_HEIGHT; this.v.y *= -1; }
    },
});

Flow

ざっくりとした流れは

  • メインシーンに切り替え
  • パーティクルを生成
  • パーティクル間の距離を調べて一定距離内ならラインを描画

ってな感じです.

Tips

下記 tips です.

times で for ループ

tmlib.js では Number を拡張して Number.prototype.times というメソッドを追加しています.
Ruby 使ってる人は馴染みがあるのではないでしょうか?

使いかたは

var num = 10;  
num.times(function(i) { console.log(i); }); // 0~9 までの数値が表示される  

ってな感じ, 数値分ループして渡した関数が実行されます.

[runstant]

今回の例ではパーティクルを生成する際やラインを描画する際に使用しています.

// パーティクルを生成
(CONFIG.particles).times(function() {
    var p = Particle().addChildTo(this.particleGroup);
    p.setPosition(Math.rand(0, SCREEN_WIDTH), Math.rand(0, SCREEN_HEIGHT));
}, this);
// ライン描画
particles.each(function(particle, i) {  
    // ポインタの位置との距離判定&ライン描画
    this.drawLine(canvas, p, particle);

    // 自分以外のパーティクルとの距離判定&ライン描画
    (particles.length-i-1).times(function(j) {
        var other = particles[j+i+1];
        this.drawLine(canvas, particle, other);
    }, this);
}, this);

draw を上書き

tm.display.CanvasElement や tm.display.Sprite, tm.display.Label といった描画機能を持ったクラスは
draw というメソッドを上書くことで独自の描画を行うことができます.

注意点としては Sprite や Label は, 画像やテキストを描画するメソッドが draw に入っているので 上書くとそれらが表示されなくなります.

CanvasElement の draw メソッドはもともと空メソッドなので上書いても問題ないです.

今回の例では, particleGroup という CanvasElement の draw メソッドを上書いて ラインを表示するメソッドを登録しています.

// グループの描画を上書き
this.particleGroup.draw = this.drawLines.bind(this);  

ランダムな方向を向いたベクトルを生成

tmlib.js には tm.geom.Vector2 というクラスが定義されており,
このクラスがもつ random という static メソッドで簡単にランダムな方向を向いた ベクトルを生成することができます.

var v = tm.geom.Vector2.random(角度の範囲start, 角度の範囲end, ベクトルの長さ);  

今回はパーティクルが移動する向きを指定する際に使用しています.

// ランダムな方向へのベクトル
this.v = tm.geom.Vector2.random(0, 360, CONFIG.speed);  

0~360 つまり全方向に向かって CONFIG.speed の速さで移動するベクトルを生成しています.

画面から出ないよう制御

よくあるやつですね. パーティクルが移動した際に, 画面からはみ出そうになったら 位置を補正して向きベクトルを反転させています.

// 画面から出ないよう制御
if (this.x < 0) { this.x = 0; this.v.x *= -1; }  
else if (this.x > SCREEN_WIDTH) { this.x = SCREEN_WIDTH; this.v.x *= -1; }  
if (this.y < 0) { this.y = 0; this.v.y *= -1; }  
else if (this.y > SCREEN_HEIGHT) { this.y = SCREEN_HEIGHT; this.v.y *= -1; }  

Fork demo

デモの CONFIG パラメータを変えたりすることで簡単に違った見た目にすることができます.

スピードアップ

CONFIG の speed を変えただけ

[runstant]

ラインの色を変更

CONFIG の shadowColor を変更

[runstant]

カラフル

それぞれのパーティクルの色を変えることでカラフルに!!

[runstant]

蜘蛛

@simiraaaa さんが気持ち悪いの作ってくれましたw それにしても, 配色含め絶妙に気持ち悪い

[runstant]