できる気になっているJSを改めてチュートリアルからやってみる 18日目
~~ ボールを弾ませよう!というプロダクト ~~
最近ちょっとずつまた学びなおす必要が出てきたなぁと思い、いろいろプログラムを勉強しなおしているところなんですが、実はもう今使っている知識は古いのかもと思って、アップデートしようとおもいやってみる会。
実施するのは、この記事
完全な初心者向けと書かれたチュートリアルは全然初心者向けではないって話。アップデートしていきましょう。
JavaScript オブジェクト入門 をやってます。
ボールを弾ませよう
Canvas APIを使い画面アニメーションさせるのに requestAnimationFrameを使ってやります。
random 関数を作成。
function random(min, max) {
const num = Math.floor(Math.random() * (max - min + 1)) + min;
return num;
}
プログラム用のボールを一つモデル化する
まずはコンストラクタでballを使う。
function Ball(x, y, velX, velY, color, size) {
this.x = x;
this.y = y;
this.velX = velX;
this.velY = velY;
this.color = color;
this.size = size;
}
・x,y座標 - ボールが画面のどこからスタートするか表す水平と垂直の座標。これは 0 からブラウザビューポートの幅と高さの間の値をとります。
・水平と垂直方向の速度(velX , velY)
・color - 個々のボールに色を付けられる
・size - 個々のボールには大きさを使えます
ボールを描画する
まず draw() メソッドを Ball() のプロトタイプ( prototype )に追加。
Ball.prototype.draw = function(){
ctx.beginPath();
ctx.fillStyle = this.color;
ctx.arc(this.x , this.y , this.size, 0, 2 * Math.PI);
ctx.fill();
}
上の関数ですが、
・beginPath() を使って紙に形を書きたいと宣言
・次に fillStyle を使って形を何色にしたいか宣言します。
・次に arc() メソッドを使って紙に円弧形をなぞります。
・円弧の中心座標 - x , y
・円弧の半径 - size
・最後の2つの引数は円弧の開始点から終了点までの角度を円の中心核で指定します。ここでは 0 度から 2 * PI、これはラジアンで表した 360度に相当します。
これでテストができるようになりました。その後、ブラウザで開いて以下のコードを打つとボールが表示されます。
var testBall = new Ball(50, 100, 4, 4, 'blue', 10);
testBall.x
testBall.size
testBall.color
testBall.draw()
という感じですね。
ボールのデータを更新する
ボールを座標に表示できるようになりましたが、ボールを実際移動させるには何らかの更新するための関数が必要です。JavaScriptファイルの最後に以下のコードを追加し、 update() メソッドを Ball() の prototypeに追加します。
Ball.prototype.update = function(){
if((this.x + this.size) >= width){
this.velX = -(this.velX);
}
if((this.x - this.size) <= 0){
this.velX = -(this.velX);
}
if((this.y + this.size) >= width){
this.velY = -(this.velY);
}
if((this.y - this.size) <= 0){
this.velY = -(this.velY);
}
this.x += this.velX;
this.y += this.velY;
}
関数の頭から4つの部分でボールがキャンバスの端に達したかどうかチェックします。ボールが上方向に移動していたならば(velYが正)、垂直方向の速度をボールがした方向に移動するように変更します(velYを負)。
・x座標がキャンバスの幅より大きいかチェック(ボールは右端から飛び出そうとしている)
・x座標が 0 より小さいかチェック(ボールは左端から飛び出そうとしている)
・y座標がキャンバスの高さより大きいかチェック(ボールは下端から飛び出そうとしている)
・y座標が 0 より小さいかチェック(ボールは上端から飛び出そうとしている)
さぁここまででいったんアニメーションさせちゃお。
アニメーションさせちゃお
最初にボールを全部保存しておく場所を作ります。
let balls = [];
んでこの関数はってといわれたので貼ります。
function loop() {
ctx.fillStyle = 'rgba(0, 0, 0, 0.25)';
ctx.fillRect(0, 0, width, height);
while (balls.length < 25) {
var size = random(10,20);
var ball = new Ball(
// ball position always drawn at least one ball width
// away from the adge of the canvas, to avoid drawing errors
random(0 + size,width - size),
random(0 + size,height - size),
random(-7,7),
random(-7,7),
'rgb(' + random(0,255) + ',' + random(0,255) + ',' + random(0,255) +')',
size
);
balls.push(ball);
}
for (var i = 0; i < balls.length; i++) {
balls[i].draw();
balls[i].update();
}
requestAnimationFrame(loop);
}
loop();
いろいろ書いてあったのですが、やっていることは以下。
・キャンバスの塗りつぶし色を半透明の黒にして、その色でキャンバスの幅と高さいっぱいの長方形を fillRect() を描きます。
・fillRectで前のフレームで書いた内容を見えなくします。(0.25でopatcity を調整しているので、これを薄くすれば残りやすくなるし、1にすれば残像0)
・次にrandom() 関数で作成したランダムな値を使って新しい Ball() インスタンスを作成します。
・push()で作ったballs配列にぶち込む
・25個以下になったらやめる
・balls配列のボール全部見に行って draw() , update() 関数を実行して次のフレームに備えて必要な位置と速度の更新を行う。
・この関数を requestAnimationFrame() メソッドを使ってメソッドが定期的に実行され同じ関数名を与えられると、その関数がスムースなアニメーションを行うために毎秒設定された回数実行されます。
いい感じでしたわ。
衝突判定
つぎに update() メソッドを定義したすぐ下に以下を追加します。
・それぞれのボールで、ほかのボールそれぞれ床のボールが衝突していないか調べるために、ballsすべてを確認するためのforループを回す。
・if文でループで回しているボールがチェックしているボールと同じかを調べます。
・!を使って等価性チェックを逆にしそのボール自身でない場合だけチェックを走らせるようにします。
・そして、一般的に2つの円が衝突していないか調べるための一般的なアルゴリズムを使っています。
・もし衝突が検出されたら内側のif( distance < this.size + balls[j].size ) が実行されて、新しい色を設定するようにします。
※ぶつかったとかの計算はライブラリをつかいましょ。
最終的なJSはこんなかんじ
// setup canvas
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
const width = canvas.width = window.innerWidth;
const height = canvas.height = window.innerHeight;
// function to generate random number
function random(min, max) {
const num = Math.floor(Math.random() * (max - min + 1)) + min;
return num;
}
function Ball(x, y, velX, velY, color, size) {
this.x = x;
this.y = y;
this.velX = velX;
this.velY = velY;
this.color = color;
this.size = size;
}
Ball.prototype.draw = function () {
ctx.beginPath();
ctx.fillStyle = this.color;
ctx.arc(this.x, this.y, this.size, 0, 2 * Math.PI);
ctx.fill();
}
Ball.prototype.update = function () {
if ((this.x + this.size) >= width) {
this.velX = -(this.velX);
}
if ((this.x - this.size) <= 0) {
this.velX = -(this.velX);
}
if ((this.y + this.size) >= width) {
this.velY = -(this.velY);
}
if ((this.y - this.size) <= 0) {
this.velY = -(this.velY);
}
this.x += this.velX;
this.y += this.velY;
}
Ball.prototype.collisionDetect = function () {
for (var j = 0; j < balls.length; j++) {
if (!(this === balls[j])) {
var dx = this.x - balls[j].x;
var dy = this.y - balls[j].y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < this.size + balls[j].size) {
balls[j].color = this.color = 'rgb(' + random(0, 255) + ',' + random(0, 255) + ',' + random(0, 255) + ')';
}
}
}
}
let balls = [];
function loop() {
ctx.fillStyle = 'rgba(0, 0, 0, 0.25)';
ctx.fillRect(0, 0, width, height);
while (balls.length < 25) {
var size = random(10, 20);
var ball = new Ball(
// ball position always drawn at least one ball width
// away from the adge of the canvas, to avoid drawing errors
random(0 + size, width - size),
random(0 + size, height - size),
random(-7, 7),
random(-7, 7),
'rgb(' + random(0, 255) + ',' + random(0, 255) + ',' + random(0, 255) + ')',
size
);
balls.push(ball);
}
for (var i = 0; i < balls.length; i++) {
balls[i].draw();
balls[i].update();
balls[i].collisionDetect();
}
requestAnimationFrame(loop);
}
loop();
以上。各フレームごとにアニメーションをするのか。べんきょうになりました。