前回 の通り、「(オリジナル) はブラウザー用に書かれているので HTML (index.html)と JavaScript (boids.js)により書かれて 」いるので先ず、
【Task1】
私はHTML、JavaScript共に門外漢で、先ずはそのコードの意味と「何が何をしているのか」の解明をすることが先決となりました。
先ずはエントリーポイントとなるHTMLファイルを眺めます。
【index.html】
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Boids</title>
<script src=" ./boids.js "></script> (解説:ここでJavaScriptファイルを実行するようです。)
<style type="text/css"> (解説:ページ全体にCSS(スタイルシート)を適用する、らしいです。)
body {
margin: 0;
background: #282c34;
overflow: hidden;
}
</style>
</head>
<body>
<canvas id="boids" width="150" height="150"></canvas>
</body>
</html>
今度はJavaScriptファイルを眺めますが、英語表記なので(今後C#用にする可能性もあり、) 拡張子を".cs "にしてコメントを日本語で書いてみます。(最初は意訳でしたが、その後少しこんなもの で調べて内容を確かめました。)
【boids. cs 】
//////////////////////////////////////////////////
// Boids algorithm demonstration (in JavaScript)
// Original program Copyright by Ben Eater
// 日本語コメント文責:Y-sama
// https://eater.net/boids
//////////////////////////////////////////////////
//描画領域の幅、高さ(ブラウザーの大きさに合わせて更新される)
let width = 150; //HTMLファイルの描画領域canvas定義の初期値
let height = 150; //HTMLファイルの描画領域canvas定義の初期値
//定数
const numBoids = 100; //擬鳥の数
const visualRange = 75; //視野
var boids = []; //擬鳥配列(解説:変数の型は不問)
//擬鳥初期化(解説:構造体の宣言がありませんが、ループを使った初期化で構造体の変数としているようです。)
function initBoids() {
for (var i = 0; i < numBoids; i += 1) {
//解説:構造体の要素は描画にx, yが使われることから整数の様ですが、randome()は倍制度実数なのでそうなのかも?
boids[boids.length] = { //配列を代入するとlengthが自動的に+1される(解説:{}括弧内が要素のようです。
x: Math.random() * width, //x座標(Math.random()は静的メソッドで、 0 以上 1 未満の範囲で浮動小数点の擬似乱数を返す)
y: Math.random() * height, //y座標
dx: Math.random() * 10 - 5, //x移動距離(-5~5-左右へ5)
dy: Math.random() * 10 - 5, //y移動距離(-5~5-上下へ5)
history: [], //履歴(x、y座標の履歴配列-↓を見て後で分かりました。)
};
}
}
//擬鳥間の距離計算(√(x軸距離 ^ 2 + y軸距離 ^ 2))(解説:boids1, 2の方が整数なのか、実数なのかわかりません。)
function distance(boid1, boid2) {
return Math.sqrt( //解説:Math.sqrt()の引数は倍精度実数となります。
(boid1.x - boid2.x) * (boid1.x - boid2.x) +
(boid1.y - boid2.y) * (boid1.y - boid2.y),
);
}
//TODO: 未熟で非効率的なので要改善
function nClosestBoids(boid, n) {
//擬鳥のコピー作成(.slice()に引数を与えないと全ての要素となる)
const sorted = boids.slice(); //配列のメソッドで、配列の一部を start から end (end は含まれない)までの範囲で、選択した新しい配列オブジェクトにシャローコピーして返す。
//自分からの距離に応じて他の擬鳥をソートする
sorted.sort((a, b) => distance(boid, a) - distance(boid, b));
// Return the `n` closest
return sorted.slice(1, n + 1); //自分自身が0になるので、次(1)からn迄のシャローコピー配列となる
}
//初期化及びサイズ変更時に描画領域とwidth/heightフィールドを変更する。
function sizeCanvas() {
/* 解説:以下は私の参照用に入れていました。
getElementById(id)はDocumentインターフェイスのメソッドで、idで指定された文字列に
一致するElementオブジェクトを返します。
【HTMLファイルの関連表記】
"<canvas id="boids" width="150" height="150"></canvas>"
<canvas>:描画領域(キャンバス)を定義するHTMLタグ
id="boids":このキャンバスをJavaScriptで操作・特定するための固有の識別名。
width:キャンバスの横幅
height:キャンバスの縦幅
*/
const canvas = document.getElementById("boids"); //HTMLからidが"boids"のcanvas要素を取得する
width = window.innerWidth; //ウィンドウ幅をwidthに取得(グローバル変数のwindowは、スクリプトを実行しているウィンドウ)
height = window.innerHeight; //ウィンドウ高さをheightに取得
canvas.width = width; //"boids"canvasの幅設定
canvas.height = height; //"boids"canvasの高さ設定
}
//擬鳥を描画領域内に止め、境界に接近すると反対方向へ向かわせる(dx、dyは移動方向・距離)
function keepWithinBounds(boid) {
const margin = 200; //余白(HTMLファイル上では初期値は0)
const turnFactor = 1; //方向変換要素
if (boid.x < margin) { //x軸左方向
boid.dx += turnFactor; //右方向(加算)
}
if (boid.x > width - margin) { //x軸右方向
boid.dx -= turnFactor //左方向(減算)
}
if (boid.y < margin) { //y軸上方向
boid.dy += turnFactor; //下方向(加算)
}
if (boid.y > height - margin) { //y軸下方向
boid.dy -= turnFactor; //上方向(減算)
}
}
//擬鳥の集団の中央を探し、そこへ向かう速度を少し調節する
function flyTowardsCenter(boid) {
const centeringFactor = 0.005; //速度変更係数( 解説:型指定はないが実数)
let centerX = 0;
let centerY = 0;
let numNeighbors = 0;
for (let otherBoid of boids) { //解説:C#なら"foreach(Boid otherBoid in boids)"的ループ?
//視野内の擬鳥のx、y座標とその数を加算
//otherBoidにboidが含まれる為、if (otherBoid !== boid) {}が必要か?
if (distance(boid, otherBoid) < visualRange) {
centerX += otherBoid.x; //解説:整数で可
centerY += otherBoid.y; //解説:Ditto
numNeighbors += 1;
}
}
if (numNeighbors) { //解説:視界内に擬鳥がいれば
//視野内の擬鳥のx、y座標の平均値を求める
centerX = centerX / numNeighbors; //解説:整数で可(切り捨てになるが)
centerY = centerY / numNeighbors; //解説:Ditto
//自分のdx、dyに集団の中心との平均距離に速度変更係数を乗じたものを加える
boid.dx += (centerX - boid.x) * centeringFactor; //解説:計算式部分は実数で、代入は整数とすることが必要
boid.dy += (centerY - boid.y) * centeringFactor; //解説:Ditto
}
}
//衝突しそうな近くの擬鳥から離れる
function avoidOthers(boid) {
const minDistance = 20; //離反する境界距離
const avoidFactor = 0.05; //速度変更係数( 解説:型指定はないが実数)
let moveX = 0;
let moveY = 0;
for (let otherBoid of boids) {
if (otherBoid !== boid) { //解説:ここでは自分自身を除外している(↑flyTowardCenter参照)
//境界距離以内の擬鳥のx、y座標を自分の座標から減算
if (distance(boid, otherBoid) < minDistance) {
moveX += boid.x - otherBoid.x; //解説:moveX -= "otherBoid.x - boid.x;"に等しい
moveY += boid.y - otherBoid.y; //解説:Ditto
}
}
}
//自分のdx、dyに離反座標に速度変更係数を乗じたものを減じる(マイナスを加算する)
boid.dx += moveX * avoidFactor;
boid.dy += moveY * avoidFactor;
}
//他の擬鳥の速さ(速度と方向)を求め、それへ自分を調整する
function matchVelocity(boid) {
const matchingFactor = 0.05; //速度変更係数( //解説:型指定はないが実数)
let avgDX = 0;
let avgDY = 0;
let numNeighbors = 0;
for (let otherBoid of boids) {
//視野内の擬鳥のx、y移動距離を加算し、その数を記録
if (distance(boid, otherBoid) < visualRange) {
avgDX += otherBoid.dx;
avgDY += otherBoid.dy;
numNeighbors += 1;
}
}
//移動距離の合計を総数で除算して平均を求め、自分の移動距離との差に速度変更係数を乗じたもので調整する
if (numNeighbors) { //解説:視野内に擬鳥がいれば
avgDX = avgDX / numNeighbors; //解説:整数で可
avgDY = avgDY / numNeighbors; //解説:Ditto
boid.dx += (avgDX - boid.dx) * matchingFactor; //解説:計算式は実数で、整数で代入
boid.dy += (avgDY - boid.dy) * matchingFactor; //解説:Ditto
}
}
//集合速度は自然と変化するが、現実には意図的に変化させてはいない
function limitSpeed(boid) {
const speedLimit = 15; //限界速度( 解説:整数で可)
//x、y軸の移動距離から求められる速度(speed)が
const speed = Math.sqrt(boid.dx * boid.dx + boid.dy * boid.dy); //解説:Mathsqrt()の引数は倍制度実数
//限界速度を超えたならば、x、y軸の移動距離を限界速度で頭打ちにする
if (speed > speedLimit) {
boid.dx = (boid.dx / speed) * speedLimit; //解説:整数不可(整数だと0か1になる)
boid.dy = (boid.dy / speed) * speedLimit; //解説:Ditto
}
}
const DRAW_TRAIL = false; //飛行跡
function drawBoid(ctx, boid) { //擬鳥の描画
const angle = Math.atan2(boid.dy, boid.dx); //Math.atan2(y, x) に対して点 (0, 0) から点 (x, y) までの半直線と、正の x 軸の間の平面上での角度(ラジアン単位)を返す(C#の Math.Atan2)
//解説:以下の描画表記はOpenGLを彷彿とさせます。
ctx.translate(boid.x, boid.y); //x、y座標へ移動
ctx.rotate(angle); //angle分回転
ctx.translate(-boid.x, -boid.y);
ctx.fillStyle = "#558cf4"; //キャンバス 2D API のプロパティで、図形の内側を塗りつぶすために使用する色、グラデーション、またはパターンを指定
ctx.beginPath();
ctx.moveTo(boid.x, boid.y); //以下座標移動し、三角形を描画し、塗り潰す
ctx.lineTo(boid.x - 15, boid.y + 5);
ctx.lineTo(boid.x - 15, boid.y - 5);
ctx.lineTo(boid.x, boid.y);
ctx.fill();
ctx.setTransform(1, 0, 0, 1, 0, 0);
if (DRAW_TRAIL) { //飛行跡が真であれば一定の長さの飛行痕(糸のような線)を残す
ctx.strokeStyle = "#558cf466";
ctx.beginPath();
ctx.moveTo(boid.history[0][0], boid.history[0][1]);
for (const point of boid.history) {
ctx.lineTo(point[0], point[1]);
}
ctx.stroke(); //キャンバス 2D API のメソッドで、現在のあるいは渡されたパスを、現在の線のスタイルで描画
}
}
//Mainループ
function animationLoop() {
//すべての擬鳥を更新
for (let boid of boids) {
//速さをそれぞれの規則に基づいて更新
flyTowardsCenter(boid); //群れへの参集
avoidOthers(boid); //近すぎる擬鳥からの離反
matchVelocity(boid); //速度調整
limitSpeed(boid); //限界速度制限
keepWithinBounds(boid); //枠内制限
//現在の速さに基づき位置(x、y座標)を更新
boid.x += boid.dx;
boid.y += boid.dy;
boid.history.push([boid.x, boid.y]) //配列の末尾に指定された要素を追加
boid.history = boid.history.slice(-50); //slice() は Array インスタンスのメソッドで、配列の一部を start から end (end は含まれない)までの範囲で、選択した新しい配列オブジェクトにシャローコピーして返す
//引数が負の場合、配列の末尾からさかのぼって数えるので、キューとして使っている
}
//描画領域を一旦消去し、擬鳥を新しい位置で描画
const ctx = document.getElementById("boids").getContext("2d");
ctx.clearRect(0, 0, width, height);
/* サンプル( 解説:自分用の備忘です。)
const canvas = document.getElementById("canvas"); //HTMLから canvas 要素を取得する
const ctx = canvas.getContext("2d"); //平面(2次元)描画を指定
ctx.beginPath(); // 新しいパスを開始
ctx.rect(10, 20, 150, 100); // 矩形を現在のパスに追加
ctx.fill(); // パスを描画
*/
for (let boid of boids) {
drawBoid(ctx, boid);
}
//次の描画フレームを予約
window.requestAnimationFrame(animationLoop);
/* アニメーションを実行したいことをブラウザーに指示します。
このメソッドは、次回の再描画の前に、ユーザーが指定した
コールバック関数を呼び出すようブラウザーに要求します。
コールバック関数への呼び出し頻度は、通常、ディスプレイの
リフレッシュレートと一致します。 最も一般的なリフレッシュ
レートは 60Hz(60 サイクル/フレーム毎秒)ですが、75Hz、
120Hz、144Hz も広く使用されています。
*/
}
window.onload = () => { //WM_CREATE処理
//描画領域が何時もウィンドウサイズとなるように、サイズ変更された場合にsizeCanvasを呼び出す
window.addEventListener("resize", sizeCanvas, false);
sizeCanvas();
//擬鳥の初期化を行う
initBoids();
//アニメーション動画の予約
window.requestAnimationFrame(animationLoop);
};
JavaScriptのプログラム、如何でしたか?私も初めてなのですが、何かOOP(Object Oriented Program)というよりも、手続型のC言語にOpenGLを混ぜたような感じがしませんか?
所で、JavaScriptのファイルの拡張子wo".cs "にしたので、このままではHTMLファイルが動きません。冒頭↑で参照したのHTMLファイルも次のように修正します。
<script src="./boids. cs "></script>
こんなことしてJavaScriptが動くのかって?
index.htmlファイルをダブルクリックすると、何事もなかったかのように
擬鳥が飛びます。
さーて、
どういうプログラムかは大体わかったけれど、言語はJavaScript、でもプログラム自体は完成している(このまま楽しむなら弄る必要はない) し、
今後どうしましょうか? (要すれば この間書いた 【Task2】と【Task3】をやるか否か、ということです。)
ps. まずはどうあれC#に移植してから、プログラム拡張を考えるってことかなぁ?