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
Drag mouse to navigate.
Navigation
- Left/Right Mouse drag: Changes camera heading.
- Up/Sown Mouse drag: Changes camera pitch.
- Scroll wheel: Changes camera field of view.
- I-Key: Displays Info panel with canvas size, image size and FPS.
17.Aug.2010, Martin Wengenmayer