DIR : /home/kozerus/public_html/jkwii/js/jsdoit/boidswar/index.js

/home/kozerus/public_html/jkwii/js/jsdoit/boidswar

/**
 * Using Point class
 * @see http://jsdo.it/akm2/fhMC
 */
 
/**
 * click: Add Boid.
 * keyboard 'g': Remove Pink Boid.
 * keyboard 'p': We are the world.
 * keyboard 'd': Show debug maker.
 */
 
var FPS = 60;
var MAX_SPEED = 2.35;
var canvas;
var canvasWidth = 0;
var canvasHeight = 0;
var context;
var boidTypes;
var boids = [];
var bloodstains = [];
var mousePoint = new Point();
var clickCount = 0;
var isPeace = false;

// Alias
var random = Math.random;

function init() {
    canvas = document.getElementById('c');
    context = canvas.getContext('2d');
    context.lineCap = 'round';
    
    window.addEventListener('resize', resize, false);
    resize();
    
    // Boid の種類を設定
    // BoidType('Boid の識別名', '獲物の識別名', '天敵の識別名', '色')
    // 獲物と天敵を複数指定の場合は配列で指定
    boidTypes = [
        new BoidType('a', 'b', ['c', 'd'], 'rgba(250, 185, 15, 1)'), // Orange
        new BoidType('b', 'c', ['a', 'd'], 'rgba(165, 240, 20, 1)'), // Green
        new BoidType('c', 'a', ['b', 'd'], 'rgba(20, 135, 240, 1)'), // Blue
        new BoidType('d', ['a', 'c', 'b'], false, 'rgba(255, 0, 255, 1)') // Pink
    ];
    
    for (var i = 0; i < 30; i++) {
        createBoid(random() * innerWidth, random() * innerHeight, boidTypes[Math.floor(i / 10)]);
    }
    
    document.addEventListener('mousemove', mouseMove, false);
    document.addEventListener('mouseout', mouseOut, false);
    document.addEventListener('click', click, false);
    document.addEventListener('keydown', keyDown, false);
    
    setInterval(loop, 1000 / FPS);
}

function resize() {
    canvas.width = canvasWidth = window.innerWidth;
    canvas.height = canvasHeight = window.innerHeight;
}

function mouseMove(e) {
    mousePoint.x = e.clientX;
    mousePoint.y = e.clientY;
}

function mouseOut(e) {
    mousePoint.x = mousePoint.y = -9999;
}

function click(e) {
    // クリックごとに Boid の種類を切り替える
    // まれにピンクボイド (インデックス 3) を出現させる, ピンクは出現数を 1 に
    var typeIndex = clickCount % (3 + Math.floor(Math.random() * 0.9 + 0.2));
    var n = typeIndex === 3 ? 1 : 3;
    //var typeIndex = clickCount % boidTypes.length;
    //var n = boidTypes.length;
    
    for (var i = 0; i < n; i++) {
        createBoid(e.clientX, e.clientY, boidTypes[typeIndex]);
    }
    clickCount++;
}

function keyDown(e) {
    if (e.keyCode === 71) { // g key
        var boid;
        for (var i = 0, len = boids.length; i < len; i++) {
            boid = boids[i];
            if (boid.type.name === 'd') {
                bloodstains.push(boid.createBloodstain());
                boids.splice(i, 1);
                len--;
                i--;
            }
        }
    }
    
    if (e.keyCode === 80) { // p key
        isPeace = !isPeace;
    }
    
    // Debug mode toggle
    if (e.keyCode === 68) { // d key
        Debug.enabled = !Debug.enabled;
    }
}

function loop(){
    // Debug
    // if (Debug.loopStop()) return;
    
    context.save();
    context.fillStyle = 'rgb(245, 245, 245)';
    context.fillRect(0, 0, innerWidth, innerHeight); 
    context.restore();

    var i;
    var len;
    
    for (i = 0, len = bloodstains.length; i < len; i++) {
        var bloodstain = bloodstains[i];
        bloodstain.draw(context);
        bloodstain.update();
        
        if (bloodstain.isCleanUp) {
            bloodstains.splice(i, 1);
            len--;
            i--;
        }
    }
    
    for (i = 0, len = boids.length; i < len; i++) {
        var boid = boids[i];
        boid.draw(context);
        boid.update(boids);
        
        if (boid.isDead) {
            bloodstains.push(boid.createBloodstain());
            boids.splice(i, 1);
            len--;
            i--;
        }
    }
}

function createBoid(x, y, type) {
    var boid = new Boid(x, y, type, random() * 0.6 - 0.3, random() * 0.6 - 0.3);
    boids.push(boid);
    return boid;
}


/**
 * Boid
 */
function Boid(x, y, type, vx, vy) {
    this.x = x || 0;
    this.y = y || 0;
    this.type = type;
    this.v = new BoidVelocity(this, vx, vy);
    this.isDead = false;
    this._time = new Date().getTime();
    
    // コスト削減の使い回し
    this._op = {}; // 画面端を超えた Boid の距離計算用
    this._dv = this.v.clone(); // ベクトル描画用
}

Boid.prototype = {
    
    update: function(targets) {
        if (this.isDead) return;
        
        var len = targets.length;
        var v = this.v;
        var type = this.type;
        var overPoint = this._op;

        for (var i = 0; i < len; i++) {
            var target = targets[i];

            if (target === this) continue;
            
            var distance = Point.distance(this, target);
            var overDist = Infinity;
            
            // 画面端を超えた Boid の距離計算  
            // 超過があれば超過分の距離を算出
            var left = 80;
            var right = canvasWidth - left;
            var top = left;
            var bottom = canvasHeight - top;
            
            overPoint.x = target.x;
            overPoint.y = target.y;
            if (this.x < left && target.x > right) {
                overPoint.x -= canvasWidth;
            } else if (this.x > right && target.x < left) {
                overPoint.x += canvasWidth;
            }
            if (this.y < top && target.y > bottom) {
                overPoint.y -= canvasHeight;
            } else if (this.y > bottom && target.y < top) {
                overPoint.y += canvasHeight;
            }
            if (overPoint.x !== target.x || overPoint.y !== target.y) {
                overDist = Point.distance(this, overPoint);
            }
            
            var isOver = false;
            if (type.isFamily(target.type) || isPeace) {
                // Is family
                
                if (distance < 25 || (isOver = overDist < 25)) {
                    v.separation(isOver ? overPoint : target, isOver ? overDist : distance);
                    //Debug.mark(this, target, isOver ? overDist : distance, isOver ? overPoint : null);

                } else if (distance < 35 || overDist < 35) {
                    v.alingment(target.v);
                    //Debug.mark(this, target, isOver ? overDist : distance, isOver ? overPoint : null);

                } else if (distance < 100 || (isOver = overDist < 100)) {
                    v.cohesion(isOver ? overPoint : target);
                    //Debug.mark(this, target, isOver ? overDist : distance, isOver ? overPoint : null);
                }

            } else if (distance < 120 || (isOver = overDist < 120)) {
                if (type.isTarget(target.type)) {
                    // Is target
                    
                    v.cohesion(isOver ? overPoint : target, 2);
                    Debug.mark(this, target, isOver ? overDist : distance, isOver ? overPoint : null);

                } else if (type.isEnemy(target.type)) {
                    // Is enemy
                    
                    // 追いつかれたら死亡, 出現から1秒は無敵
                    if (new Date().getTime() - this._time > 1000 && (distance < 10 || overDist < 10)) {
                        this.isDead = true;
                        return;
                    }
                    
                    if (distance < 100 || (isOver = overDist < 100)) {
                        v.separation(isOver ? overPoint : target, isOver ? overDist : distance, 3);
                        //Debug.mark(this, target, isOver ? overDist : distance, isOver ? overPoint : null);
                    }
                }
            }
        }
        
        // マウスを避ける
        var mouseDist = Point.distance(this, mousePoint);
        if (mouseDist < 80) {
            v.separation(mousePoint, mouseDist, 5);
        }

        v.applyVelocity();
        this.x += v.x;
        this.y += v.y;
        
        // 反対側に出現
        if (this.x < 0) {
            this.x += canvasWidth;
        } else if (this.x > canvasWidth) {
            this.x -= canvasWidth;
        }
        if (this.y < 0) {
            this.y += canvasHeight;
        } else if (this.y > canvasHeight) {
            this.y -= canvasHeight;
        }
    },
    
    createBloodstain: function() {
        return new Bloodstain(this.x, this.y, this.type.color);
    },
    
    draw: function(ctx) {
        var x = this.x;
        var y = this.y;
        var v = this.v;
        var color = this.type.color;
        
        ctx.fillStyle = color;
        ctx.beginPath();
        ctx.arc(x, y, 6, 0, Math.PI * 2, false);
        ctx.fill();
        
        // ベクトルの描画
        var dv = this._dv;
        dv.x = v.x;
        dv.y = v.y;
        dv.normalize(20 * v.length() / MAX_SPEED);
        ctx.strokeStyle = color;
        ctx.beginPath();
        ctx.moveTo(x, y);
        ctx.lineTo(x + dv.x, y + dv.y);
        ctx.stroke();
    }
};


/**
 * Boid の速度を管理するオブジェクト
 * みっつのルールから速度を蓄積し applyVelocity で速度を適用する
 * 
 * @param targetBoid 担当する Boid インスタンス
 * @param x
 * @param y
 * @super Point
 * @see http://jsdo.it/akm2/fhMC
 */
function BoidVelocity(targetBoid, x, y) {
    this.target = targetBoid;
    this.x = x || 0;
    this.y = y || 0;
    this.dispose();
}

BoidVelocity.prototype = extend({}, Point.prototype, {
    
    separation: function(point, distance, scale) {
        if (typeof scale === 'undefined') scale = 1;
        
        var p = distance < 1 ? 1 : 1 / distance;
        this.sx -= (point.x - this.target.x) * p * scale;
        this.sy -= (point.y - this.target.y) * p * scale;
        this.sc++;
    },
    
    alingment: function(velocity) {
        this.ax += velocity.x;
        this.ay += velocity.y;
        this.ac++;
    },
    
    cohesion: function(point) {
        this.cx += point.x;
        this.cy += point.y;
        this.cc++;
    },
    
    /**
     * 速度を自身に適用する, 実行後はルールによって蓄積した速度を破棄
     */
    applyVelocity: function() {
        if (this.sc) {
            this.x += this.sx / this.sc;
            this.y += this.sy / this.sc;
        }
        if (this.ac) {
            this.x += (this.ax / this.ac - this.x) * 0.125;
            this.y += (this.ay / this.ac - this.y) * 0.125;
        }
        if (this.cc) {
            this.x += (this.cx / this.cc - this.target.x) * 0.005;
            this.y += (this.cy / this.cc - this.target.y) * 0.005;
        }
        
        if (this.length() > MAX_SPEED) {
            this.normalize(MAX_SPEED);
        }
        
        this.dispose();
    },
    
    dispose: function() {
        this.sx = this.sy = this.sc = 0;
        this.ax = this.ay = this.ac = 0;
        this.cx = this.cy = this.cc = 0;
    }
});


/**
 * Boid の種類を管理するオブジェクト
 * 
 * @param name Boid の識別名
 * @param target 追跡する Boid の識別名, 文字列かそれらを格納した配列
 * @param enemy 避ける Boid の識別名, 文字列かそれらを格納した配列
 * @param color
 */
function BoidType(name, target, enemy, color) {
    this.name = name;
    this.target = target;
    this.enemy = enemy;
    this.color = color;
}

BoidType.prototype = {

    isFamily: function(name) { return this._is('name', name); },
    isTarget: function(name) { return this._is('target', name); },
    isEnemy: function(name) { return this._is('enemy', name); },
    
    _is: function(key, name) {
        if (name instanceof BoidType) {
            name = name.name;
        }
        
        if (this[key] instanceof Array) {
            for (var i = 0, len = this[key].length; i < len; i++) {
                if (this[key][i] === name) {
                    return true;
                }
            }
            
            return false;
        }
        
        return this[key] === name;
    }
};


/**
 * Bloodstain
 */
function Bloodstain(x, y, color) {
    this.x = x || 0;
    this.y = y || 0;
    this.color = color;
    this.isCleanUp = false;
    this._radius = Math.random() * 5 + 15;
    this._alpha = 1;
    this._targetAlpha = 0.1;
    this._time = new Date().getTime();
}

Bloodstain.prototype = {
    
    update: function() {
        this._alpha += (this._targetAlpha - this._alpha) * 0.015;
        
        if (this._targetAlpha === 0) {
            if (Math.abs(this._targetAlpha - this._alpha) < 0.001) {
                this.isCleanUp = true;
            }
        } else if (new Date().getTime() - this._time > 20000) {
            this._targetAlpha = 0;
        }
    },
    
    draw: function(ctx) {
        ctx.save();
        ctx.fillStyle = this.color;
        ctx.globalAlpha = this._alpha;
        ctx.beginPath();
        ctx.arc(this.x, this.y, this._radius, 0, Math.PI * 2, false);
        ctx.fill();
        ctx.restore();
    }
};


// Init
window.onload = function() {
    init();
};

// メインスクリプトここまで

//-----------------------------------------
// DEBUG
//-----------------------------------------

var Debug = {
    enabled: false,
    _loopStop: false,
    
    /**
     * Boid (boid) から対象 (target) に囲みとふたつを結ぶラインをひく
     * 
     * @param boid
     * @param target
     * @param distance 対象までの距離
     * @param targetPoint
     *  targetPoint はラインの目標値となるので対象が反対側で座標が対象の実際のものと異なる場合はその座標を x, y で持つオブジェクトを指定する
     *  省略した場合は target の値が使用される
     */
    mark: function(boid, target, distance, targetPoint) {
        // デバッグモードでない場合は抜ける, d キーで有効化
        if (!this.enabled) return;
        
        if (targetPoint == null) {
            targetPoint = target;
        }
        
        var tx = target.x;
        var ty = target.y;
        var x = boid.x;
        var y = boid.y;

        context.save();
        context.lineWidth = 0.5;
        // target circle
        context.strokeStyle = '#FF0000';
        context.beginPath();
        context.arc(tx, ty, 15, 0, Math.PI * 2, false);
        context.stroke();
        // self circle
        context.strokeStyle = '#CCCCCC';
        context.beginPath();
        context.arc(x, y, 15, 0, Math.PI * 2, false);
        context.stroke();
        // line
        var radian = Math.atan2(targetPoint.y - y, targetPoint.x - x);
        var dx = distance * Math.cos(radian);
        var dy = distance * Math.sin(radian);
        context.strokeStyle = '#FF0000';
        context.beginPath();
        context.moveTo(x, y);
        context.lineTo(x + dx, y + dy);
        context.stroke();
        if (target !== targetPoint) {
            // target と targetPoint が違うインスタンス (反対側の座標オブジェクト) の場合
            context.beginPath();
            context.moveTo(tx, ty);
            context.lineTo(tx - dx, ty - dy);
            context.stroke();
        }
        context.restore();
    },
    
    loopStop: function(stop) {
        if (typeof stop !== 'undefined') {
            this._loopStop = stop;
        }
        return !this.enabled ? false : this._loopStop;
    },
    
    logLimit: function(n) {
        if (window.logLimit) {
            window.logLimit(n);
        }
    }
};

Debug.enabled = false;
Debug.logLimit(1000);

//


koh5_pano



Your browser does not support the HTML5 canvas element.


Drag mouse to navigate.

Navigation





17.Aug.2010, Martin Wengenmayer