目次
オブジェクト
Jitterクラスを作成し、オブジェクトをインスタンス化すると、画面をもぞもぞ動きます。Casey Reas、Ben Fry共著による「Processingをはじめよう 第2版」からの出典。
sketch.js
let bug; // Jitterオブジェクト用変数
function setup() {
createCanvas(710, 400);
// Jitterオブジェクトを作成
bug = new Jitter();
}
function draw() {
background(50, 89, 100);
// Jitterオブジェクトのmove()メソッドを呼び出す
bug.move();
// Jitterオブジェクトのdisplay()メソッドを呼び出す
bug.display();
}
Jitter.js
// Jitterクラス
class Jitter {
// コンストラクタメソッド
constructor() {
// 4つのプロパティを初期化
this.x = random(width);
this.y = random(height);
this.diameter = random(10, 30);
this.speed = 1;
}
// 上下左右にランダムに少量移動する
move() {
this.x += random(-this.speed, this.speed);
this.y += random(-this.speed, this.speed);
}
// 位置(this.x, this.y)に半径this.diameterの円を描画する
display() {
ellipse(this.x, this.y, this.diameter, this.diameter);
}
}
解説
本章は、p5.js特有の機能や利用法を解説したこれまでの章と異なり、p5.jsプログラムに、オブジェクト指向プログラミングの手法を持ち込む方法を述べる章です。
Webブラウザは通常、index.htmlなどのHTMLファイルを読み込んで、画面にそこに書かれている内容を表示します。HTMLファイルに、JavaScriptファイルを読み込む記述がされていると、ブラウザはそれも読み込みます。下図はこの関係を示しています。
sketch.jsには、bug = new Jitter(); という、Jitterクラスのオブジェクトをインスタンス化するコードが書かれているので、ブラウザが読み込んだJitter.jsからJitterオブジェクトが作成されて、変数bugに代入されます。
bugオブジェクトはmove()とdisplay()というメソッドを持っているので、sketch.jsのdraw()関数からそれが呼び出されると、自分の位置をランダムに少しだけ変更し、その結果を円で表示するので、もぞもぞ動く虫のようなものが表現できます。この虫(bug)は、誰から命令された訳でもなく、自分から動いています。
このサンプルと同様の内容は「9:オブジェクト p5.js JavaScript」で読むことができます。
オブジェクト指向プログラミングについては、「オブジェクト指向プログラミングとは?」ページが参考になります。また”クラス”や”インスタンス”、”メソッド”などの用語については「オブジェクト指向のクラスってなに?専門用語から設計まで徹底解説」ページが参考になります。
複数のオブジェクト
Jitterクラスを作成し、複数のオブジェクトをインスタンス化すると、それらが画面上をもぞもぞ動きます。
sketch.js
let bug1; // Jitterオブジェクト用変数
let bug2;
let bug3;
let bug4;
function setup() {
createCanvas(710, 400);
// Jitterオブジェクトを複数作成
bug1 = new Jitter();
bug2 = new Jitter();
bug3 = new Jitter();
bug4 = new Jitter();
}
function draw() {
background(50, 89, 100);
// 各Jitterオブジェクトのmove()とdisplay()メソッドを呼び出す
bug1.move();
bug1.display();
bug2.move();
bug2.display();
bug3.move();
bug3.display();
bug4.move();
bug4.display();
}
Jitter.js
// Jitterクラス
class Jitter {
// コンストラクタメソッド
constructor() {
// 4つのプロパティを初期化
this.x = random(width);
this.y = random(height);
this.diameter = random(10, 30);
this.speed = 1;
}
// 上下左右にランダムに少量移動する
move() {
this.x += random(-this.speed, this.speed);
this.y += random(-this.speed, this.speed);
}
// 位置(this.x, this.y)に半径this.diameterの円を描画する
display() {
ellipse(this.x, this.y, this.diameter, this.diameter);
}
}
解説
このサンプルの大きな特徴は、前のサンプルとまったく同じJitterクラスを使用して、new Jitter(); を複数回呼び出すだけで、それぞれが異なる動きをするオブジェクトが作成できることです。
これを実現しているのは、Jitterクラスのプロパティです。xとyプロパティによって位置がほかのものと異なり、diameterによってサイズがほかのものと異なるオブジェクトが作成されます。
// コンストラクタメソッド
constructor() {
// 4つのプロパティを初期化
this.x = random(width);
this.y = random(height);
this.diameter = random(10, 30);
this.speed = 1;
}
オブジェクトの配列
Jitterクラスを作成し、forループを使って複数のオブジェクトをインスタンス化して配列に追加します。その配列を使用すると、オブジェクトのメソッドを呼び出すことができます。
sketch.js
let bugs = []; // itterオブジェクトの配列
function setup() {
createCanvas(710, 400);
// Jitterオブジェクトを50個作成して、配列に追加
for (let i = 0; i < 50; i++) {
bugs.push(new Jitter());
}
}
function draw() {
background(50, 89, 100);
// 配列のインデックスを使ってJitterオブジェクトを参照し、
// メソッドを呼び出す
for (let i = 0; i < bugs.length; i++) {
bugs[i].move();
bugs[i].display();
}
}
Jitter.js
// Jitterクラス
class Jitter {
// コンストラクタメソッド
constructor() {
// 4つのプロパティを初期化
this.x = random(width);
this.y = random(height);
this.diameter = random(10, 30);
this.speed = 1;
}
// 上下左右にランダムに少量移動する
move() {
this.x += random(-this.speed, this.speed);
this.y += random(-this.speed, this.speed);
}
// 位置(this.x, this.y)に半径this.diameterの円を描画する
display() {
ellipse(this.x, this.y, this.diameter, this.diameter);
}
}
解説
オブジェクトの数が多くなってくると、オブジェクトの管理には配列がよく使用されます。オブジェクトを配列に入れておくと、forループで個々のオブジェクトに簡単にアクセスできるようになります。
draw()関数のforループで、print(bugs[i])を実行すると、50個のJitterオブジェクトのその時点でのプロパティ値が表示できます。これを見ても、オブジェクトはそれぞれ、自分自身のプロパティ値を持っていることが分かります。
オブジェクト2
hbarraganによるサンプルの移植版。キャンバス上でマウスを動かすと、縦棒の矩形のスピードと位置を変えることができます。線のグループを定義しているのはMrectクラスです。
sketch.js
let r1, r2, r3, r4;
function setup() {
createCanvas(710, 400);
fill(255, 204);
noStroke();
r1 = new MRect(1, 134.0, 0.532, 0.1 * height, 10.0, 60.0);
r2 = new MRect(2, 44.0, 0.166, 0.3 * height, 5.0, 50.0);
r3 = new MRect(2, 58.0, 0.332, 0.4 * height, 10.0, 35.0);
r4 = new MRect(1, 120.0, 0.0498, 0.9 * height, 15.0, 60.0);
}
function draw() {
background(0);
r1.display();
r2.display();
r3.display();
r4.display();
r1.move(mouseX - width / 2, mouseY + height * 0.1, 30);
r2.move((mouseX + width * 0.05) % width, mouseY + height * 0.025, 20);
r3.move(mouseX / 4, mouseY - height * 0.025, 40);
r4.move(mouseX - width / 2, height - mouseY, 50);
}
MRect.js
// MRectクラス
class MRect {
constructor(iw, ixp, ih, iyp, id, it) {
this.w = iw; // 縦棒(バー)の幅
this.xpos = ixp; // 矩形のx位置
this.h = ih; // 矩形の高さ
this.ypos = iyp; // 矩形のy位置
this.d = id; // 縦棒間の距離
this.t = it; // 縦棒の本数
}
// (posX, posY)に向かって減速しながら近づく
move(posX, posY, damping) {
let dif = this.ypos - posY;
if (abs(dif) > 1) {
this.ypos -= dif / damping;
}
dif = this.xpos - posX;
if (abs(dif) > 1) {
this.xpos -= dif / damping;
}
}
// このMRectオブジェクトの縦棒(細長い矩形)を描画
display() {
for (let i = 0; i < this.t; i++) {
rect(
this.xpos + i * (this.d + this.w),
this.ypos,
this.w,
height * this.h
);
}
}
}
解説
相当の数学好きが作ったのではないかと思われるサンプルです。特にnew MRect()に指定する引数の多さや、MRectオブジェクトのmove()メソッドに渡す引数を見ると、少なからずげんなりしますが、こういう場合もシンプルなものから見ていくしかありません。
最もシンプルなのは、1つのMRectオブジェクトだけを動かして表示することです。そのためには、sketch.jsのdraw()関数内で、r1以外のすべてのMRectオブジェクトのメソッドの実行を、行頭に//を加えるなどして停めます。下図はその実行結果の例です。
そして、r1オブジェクトを作成するときのnew MRect()に渡す引数を見ます。
r1 = new MRect(1, 134.0, 0.532, 0.1 * height, 10.0, 60.0);
これらの引数は、MRectクラスのコンストラクタメソッドのパラメータに対応しています。
class MRect {
constructor(iw, ixp, ih, iyp, id, it) {
this.w = iw; // 縦棒(バー)の幅
this.xpos = ixp; // 矩形のx位置
this.h = ih; // 矩形の高さ(高さに対する比率)
this.ypos = iyp; // 矩形のy位置
this.d = id; // 縦棒間の距離
this.t = it; // 縦棒の本数
}
...
}
たとえばパラメータiwの1を10に変えて実行してみます。
r1 = new MRect(10, 134.0, 0.532, 0.1 * height, 10.0, 60.0);
下図はその実行結果の例です。iwが1のときと比べると縦棒の幅がかなり太くなっていることが分かります。
またパラメータidの10.0を2に変えて実行してみます。
r1 = new MRect(10, 134.0, 0.532, 0.1 * height, 2, 60.0);
すると、縦棒間の空きがずいぶん狭くなることが分かります。
このようにして、MRectクラスのコンストラクタメソッドに渡すパラメータの働きを個々に確認していきます。確認が終わったら、元の値に戻します。
これらのパラメータは、sketch.jsのnew MRect()でMRectオブジェクトとしてインスタンス化されるとき、そのMRectオブジェクトのプロパティの値として、そのオブジェクト固有の性質を決定づけます。その固有の性質を使ってアクションするのが、オブジェクトのメソッドです。
display()メソッドは次のように定義されています。
display() {
// 縦棒の本数分、繰り返す
for (let i = 0; i < this.t; i++) {
// 縦長の細い矩形をt本描画する
rect(
this.xpos + i * (this.d + this.w),
this.ypos,
this.w,
height * this.h
);
}
}
this.tというのは、new MRect()で渡されたitパラメータで、r1オブジェクトの場合には60.0を渡しているので、このforループは60回繰り返されることになります。
forループの中のrect(は、p5.jsのrect()関数で、受け取る4つの引数x,y,w,hを使って、位置(x,y)を左上隅とし、幅がw、高さがhの矩形を描画します。wにあたるthis.wは、new MRect()で渡されたiwパラメータで、r1オブジェクトの場合には1を渡しているので、矩形の幅は1になります。またhに当たるheight * this.hは、this.h=0.532なので、400 * 0.532 = 212.8になります。 つまり幅が1、高さが212.8の細長い矩形が、点(this.xpos + i * (this.d + this.w),this.ypos)を左上隅に描画されるのです。
this.yposは0.1 * height(パラメータiypの値)なので、40と一定です。ではthis.xpos + i * (this.d + this.w)はどういった値なのでしょう?
これは次のようなコードで確認できます。
function setup() {
createCanvas(400, 300);
background(200);
fill(0);
noStroke();
const t = 5; // 繰り返し回数 => 描画する矩形の数
const xpos = 10; // 矩形の描画を開始する、左上隅のx位置
const d = 20; // 矩形間の空きの長さ
const w = 5; // 矩形の幅(小さいので縦棒に見える)
const ypos = 30; // 矩形の左上隅のy位置
const h = 0.5 // heightに対する比率(0.5ならキャンバスの高さの50%)
for (let i = 0; i < t; i++) {
print(i * (d + w)); // 0,25,50,75,100
rect(
xpos + i * (d + w),
ypos,
w,
height * h
);
}
}
下図は描画結果と各変数との関係を示しています。
(d + w)はつねに25です。iが0のときは0 * 25も0なので、xposの10が残ります。iが1のときは、10 + 1*25 = 35、iが2のときは10 * 2*25 = 60になります。xpos + i * (d + w)、つまり開始位置 + i * (空き + 幅) は、同じものを等間隔でならべたいときによく使用されます。
次はもう1つのメソッド、move()です。このメソッドには、mouseXとmouseYに関係する数値が渡されるので、描画されるMRectオブジェクトは、マウスの動きの影響を受けることになります。
move(posX, posY, damping) {
// このオブジェクトのy位置とposYとの距離
let dif = this.ypos - posY;
// 十分に大きい場合には
if (abs(dif) > 1) {
// 減衰を適用(減速して近づく)
this.ypos -= dif / damping;
}
// このオブジェクトのx位置とposXとの距離
dif = this.xpos - posX;
// 十分に大きい場合には
if (abs(dif) > 1) {
// 減衰を適用(減速して近づく)
this.xpos -= dif / damping;
}
// this.xposがposXに近づいていくグラフを描画
plot(frameCount, posX);
addPlot2(frameCount, this.xpos);
}
このメソッドで行っているのは、このオブジェクトを(posX, posY)に減速して近づける、ということです。それは上記コードの最後に記述しているグラフの描画でも分かります。下図はグラフの例です。
目的の位置まで加速して近づいたり、減速して近づくテクニックはイージングと呼ばれます。イージングについては「4_3:応答:イージングの導入 p5.js JavaScript」で述べています。
イージングは、残りの距離にある決まった数値を掛ける(または割る)計算をつづけるので、実はいつまでたっても目的の位置に到達しません。したがって、これはもう十分に到達したと言えるだろう、というタイミングを計って、計算の実行を止める必要があります。上記コードの場合、これを行っているのが、if (abs(dif) > 1) {…のifステートメントです。
MRectオブジェクトはこのように、その作成時にも移動時にも、数値の引数を多く取ります。どのような性質を持つMRectオブジェクトを作り、どのような動きをさせるのかは作り手の工夫次第です。サンプルのr4オブジェクトは、move()メソッドのposY値に(height – mouseY)を渡しているので、マウスの上下の動きと逆になります。
継承
クラスは、別のクラスをその基盤として使用して定義できます。オブジェクト指向プログラミングの用語では、クラスは、フィールド(プロパティ)とメソッドを別のクラスから継承できる、と言います。別のクラスを継承する側のオブジェクトをサブクラスと言い、継承される側のオブジェクトをスーパークラスと言います。サブクラスはスーパークラスを拡張(extends)します。
p5.jsのInheritanceサンプルは継承のサンプルとして適切でないので、「P5.js example demonstrating ES6 classes, inheritance and method chaining」のものを拝借しています。
sketch.js
let circles = [];
function setup() {
createCanvas(400, 400);
background(0);
}
function draw() {
background(0);
for (let i = 0; i < circles.length; i++) {
// メソッドチェーン
circles[i].move().draw();
}
}
function mousePressed() {
// キャンバスの左半分をクリックしたら、
// 跳ね返る円(BouncingCircleオブジェクト)を作成する
if (mouseX > width / 2) {
circles.push(new BouncingCircle(mouseX, mouseY));
// そうでない場合は、Circleオブジェクトを作成する
}
else {
circles.push(new Circle(mouseX, mouseY));
}
}
Circle.js
// ----------------------------
// 親クラス(スーパークラス)
// ----------------------------
class Circle {
// コンストラクタ =の右辺はデフォルト値(指定されない場合はこれが設定される)
// デフォルトの直径は100、カラーは赤
constructor(x = 50, y = 50, r = 100, col = '#f00') {
this.x = x;
this.y = y;
this.r = r;
this.col = col;
this.vx = random(-5, 5); // x方向の速度
this.vy = random(-5, 5); // y方向の速度
}
move() {
this.x += this.vx;
this.y += this.vy;
return this; // circles[i].move().draw()というメソッドチェーンが可能になる
}
draw() {
fill(this.col);
ellipse(this.x, this.y, this.r);
return this; // メソッドチェーンを可能にする
}
}
// ----------------------------
// 子クラス(サブクラス)
// ----------------------------
// BouncingCircleクラスはCircleクラスを拡張する
class BouncingCircle extends Circle {
// コンストラクタ =の右辺はデフォルト値(指定されない場合はこれが設定される)
// デフォルトの直径は50、カラーは緑
constructor(x = 50, y = 50, r = 50, col = '#0f0') {
// 引数を渡して親クラスのコンストラクタを呼び出す
// これにより親クラスのプロパティ、メソッドが継承される
super(x, y, r, col);
}
// BouncingCircleクラスにはdraw()メソッドが定義されていないが、
// BouncingCircleクラスはCircleクラスを継承する、Circleクラスの子クラスなので、
// Circleクラスのdraw()メソッドが使用できる。
move() {
this.x += this.vx;
this.y += this.vy;
// この跳ね返りは親クラスにはなく、子クラス固有のもの
// 方向を反転
if (!this.onScreen()) {
this.vx *= -1;
this.vy *= -1;
}
return this; // メソッドチェーンを可能にする
}
// onScreen()メソッドはBouncingCircleクラス固有のもの。このメソッドをmove()メソッドで使用しているので、
// このクラスのオブジェクトはキャンバスの端で跳ね返ることができる。
// キャンバスの上下左右の端を超えていればfalse、超えていなければtrueを返す
onScreen() {
if (this.x - (this.r / 2) < 0 || this.x + (this.r / 2) > width || this.y - (this.r / 2) < 0 || this.y + (this.r / 2) > height) {
return false;
}
return true;
};
}
キャンバスの左半分をクリックすると、跳ね返る緑のボールが作成できます。右半分をクリックすると、跳ね返らない赤い大きなボールが作成できます。
解説
JavaScriptファイルを読み込むHTMLファイルでは、親クラスのJavaScriptを、子クラスのJavaScriptファイルよりも先に読み込む必要があります。
<script src="js/Circle.js"></script>
<script src="js/BouncingCircle.js"></script>
そうしないと、このサンプルの場合、まず”Circle is not defined”というエラーが発生し、画面右をクリックすると、”BouncingCircle is not defined”というエラーが発生します。
1つめのエラーが発生するのは、ブラウザがCircle.jsより先にBouncingCircle.jsを読み込んで解釈し、親クラスのCircleクラスを探しますが、この時点でまだCircle.jsが読み込まれていないからです。2つめのエラーが発生するのは、Circleクラスを継承するBouncingCircleクラスが正しく読み込まれていないからです。