目次
描画
生成的描画プログラム。
(注:入門的な「ハローp5」カテゴリに含まれてはいますが、内容は上級者向けです)
sketch.js
// すべてのパス
let paths = [];
// 今描いているかどうか
let painting = false;
// 次の円を作成するまでの時間
let next = 0;
// 今どこにいて、前にどこにいたのか
let current;
let previous;
function setup() {
createCanvas(720, 400);
current = createVector(0, 0);
previous = createVector(0, 0);
};
function draw() {
background(200);
// 新しい点を作成できる状況になったら
if (millis() > next && painting) {
// マウス位置を取得
current.x = mouseX;
current.y = mouseY;
// 新しいパーティクルの力はマウスの動きにもとづく
let force = p5.Vector.sub(current, previous);
force.mult(0.05);
// 新しいパーティクルを追加
paths[paths.length - 1].add(current, force);
// 次の円の予定を立てる
next = millis() + random(100);
// マウス位置を保持
previous.x = current.x;
previous.y = current.y;
}
// すべてのパスを描画する
for (let i = 0; i < paths.length; i++) {
paths[i].update();
paths[i].display();
}
}
// ここからスタート
function mousePressed() {
next = 0;
painting = true;
previous.x = mouseX;
previous.y = mouseY;
paths.push(new Path());
}
// ストップ
function mouseReleased() {
painting = false;
}
Particles.js
// Particlesクラス
class Particle {
constructor(position, force, hue) {
this.position = createVector(position.x, position.y);
this.velocity = createVector(force.x, force.y);
this.drag = 0.95;
this.lifespan = 255;
}
update() {
// 移動
this.position.add(this.velocity);
// スローダウン
this.velocity.mult(this.drag);
// フェードアウト
this.lifespan--;
}
// パーティクルを描画し、必要なら線でつなぐ
display(other) {
stroke(0, this.lifespan);
fill(0, this.lifespan / 2);
ellipse(this.position.x, this.position.y, 8, 8);
// 線を描く必要があるなら
if (other) {
line(this.position.x, this.position.y, other.position.x, other.position.y);
}
}
}
Path.js
// Pathクラス パーティクルのリスト
class Path {
constructor() {
this.particles = [];
this.hue = random(100);
}
add(position, force) {
// 位置と力、色調を持つ新しいParticleオブジェクトを作成して、自分のparticles配列に追加
this.particles.push(new Particle(position, force, this.hue));
}
// パーティクルを更新
update() {
for (let i = 0; i < this.particles.length; i++) {
this.particles[i].update();
}
}
// パーティクルを表示
display() {
// 後ろ向きにループ
for (let i = this.particles.length - 1; i >= 0; i--) {
// 削除すべきなら
if (this.particles[i].lifespan <= 0) {
this.particles.splice(i, 1);
// そうでないなら表示
}
else {
this.particles[i].display(this.particles[i + 1]);
}
}
}
}
解説
各ファイルのコードが短く、一見やさしそうに思えますが、実は複雑なことをオブジェクト指向プログラミングの方法で分散することで簡単にしているすぐれたサンプルです。
このサンプルを操作して気づく特徴は、
- マウスのクリックでクリックした位置に黒い円が描かれる
- マウスのクリック&ドラッグで、黒い円とそれをつなぐ黒い線が描かれる。円と線はドラッグの軌跡に沿って作成される
- ドラッグを終え、さらにドラッグを始めると、前とは別の円と線のつながりが描かれる
- 描いた円と線はアニメーションして移動し、やがて止まる
- 描いた円と線はフェードアウトして見えなくなる
といったことでしょうか。
3つのファイルの役割
sketch.jsとPass.js、Particle.jsにはそれぞれ役割があります。sketch.jsは言わば司令塔で、Pass.jsは自分の配列の中にParticleオブジェクトを保持し、sketch.jsからの命令に応じて、各Particleオブジェクトに命令を出します。Particle.jsは自分の位置に応じて円と線を実際に描画します。Pass.jsを飛ばしてsketch.jsからParticle.jsに直接命令を送ればよいのではないかと思われるかもしれませんが、Pass.jsには、マウスダウンからマウスリリースまでのマウスの移動経路(パス)を1つにまとめる重要な役割があります。
3つのファイルの関係性は下図のように示すことができます。
このように、できる開発者が完成させたコードを読み解いていくのも1つの方法です(当サイトでもほかのサンプルではそのようにしています)。しかし、誰でもがいきなりこのような整然としたコードが記述できるわけではなく、実際にはsketch.jsの記述から始めるはずです。そしていろいろ進めるうちにこんがらがってきてスパゲッティコード化し、「これは参ったな」と思うわけです。そこで以降では、sketch.jsだけの記述にこだわり、あえてスパゲッティコードの道を突き進んでいくことにします。なぜなら、そうすることで、コードの役割を分担するオブジェクト指向プログラミングの手法の良さがあぶり出せると思うからです。
目指せ! スパゲッティコード!
プログラミングで重要なのは、簡単なことから始め、それを確実な土台として、次の段階に進むことです。一度に2つのことをやろうとしてはいけません。不確かな事柄を残したまま次に進むと、土台が崩れ、やり直しになってしまいます。
今の場合、最初に押さえておきたい事柄は、ベクトルを使った移動する円の描画です。これには速度や加速度といった要素が絡んできます。
位置と速度、加速度
p5.jsではベクトルをp5.Vectorオブジェクトで扱えます。p5.Vectorオブジェクトについては「9:シミュレーション 1/2 (Simulate)」で述べています。
速度と加速度について、「変位と速度と加速度」ページには、次のように書かれています。
変位:物体が運動して位置が変わったときのその位置の変化量。
速度:運動のスピード。単位時間当たりの変位の変化量。変位を時間で割ったもの。
加速度:加速の度合い。単位時間当たりの速度の変化量。速度を時間で割ったもの。
また、位置と速度、加速度には次の関係があります。
- 加速度は速度を変更する
- 速度は位置を変更する
これを踏まえて、p5.Vectorを使った移動する円のコードは次のように記述できます。
// 時間。p5.jsのフレームレートはデフォルトで60fpsなので、
// 1フレーム当たりの時間は1/60秒
const t = 1 / 60;
let positionV; // 位置用ベクトル
let velocityV; // 速度用ベクトル
let accelV; // 加速度用ベクトル
function setup() {
createCanvas(400, 400);
positionV = createVector(0, 0);
// 前の位置
const previous = createVector(0, 0);
// 今の位置
const current = createVector(10, 10);
// 速度は単位時間当たりの変異の変化量
// => 変異を時間で割ったもの
velocityV = (p5.Vector.sub(current, previous)).div(t);
// 速度の調整(もっと遅くする)
velocityV.mult(0.005);
// 加速度は単位時間当たりの速度の変化量
// => 速度を時間で割ったもの
accelV = p5.Vector.div(velocityV, t);
// 加速度の調整(もっと小さくする)
accelV.mult(0.005);
}
function draw() {
background(200);
// 速度は毎回リセットする必要がある
velocityV.mult(0);
// 加速度は速度を変更する
velocityV.add(accelV);
// 速度は位置を変更する
positionV.add(velocityV);
// 描画
ellipse(positionV.x, positionV.y, 10, 10);
}
変数positionVは位置を表すベクトルで、velocityVは速度を、accelVは加速度を表すベクトルです。これらを操作することで、移動する円を描画します。
変数previousは1フレーム前の円の位置を、currentは今の円の位置を表すベクトルとします。今のフレームでの位置と前のフレームでの位置の差は、変位の変化量と考えることができます。また時間は、今の場合1フレームを扱っているので、p5.jsデフォルトの60fps = 1/60秒と考えることができます。
すると、単位時間当たりの変異の変化量であり、変異を時間で割ったものである速度は次のコードで計算できます。0.005を掛けているのは、単にキャンバスの中にうまく描画できるようにするためで、便宜的な調整です。
velocityV = (p5.Vector.sub(current, previous)).div(t);
velocityV.mult(0.005);
同様に、単位時間当たりの速度の変化量であり、速度を時間で割ったものである加速度も計算できます。
accelV = p5.Vector.div(velocityV, t);
加速度が求められたら、draw()内で円が描画できます。加速度は速度を変更し、速度は位置を変更します。円の位置はposition変数のxとyプロパティで分かります。
velocityV.mult(0);
velocityV.add(accelV);
positionV.add(velocityV);
ellipse(positionV.x, positionV.y, 10, 10);
では、以上を踏まえて、サンプルの機能を少しずつsketch.jsに実装していきましょう。
マウスクリックした位置に円を描画、ドラッグすると円はドラッグした方向に移動してやがて止まる
まずは、マウスでクリックした位置に円を描画する機能と、マウスのドラッグでその円がドラッグした方向に移動して止まる機能を作成しましょう。これは、前の特徴のリストで言うと、(1)と(4)に当たります。
let positionV; // 円の位置ベクトル
let velocityV; // 円の速度ベクトル
let forceV; // 円にかかる力のベクトル
let current; // マウスの今の位置ベクトル
let previous; // マウスの前のフレームの位置ベクトル
// 描画状態にあるかどうか
let painting = false;
// 減衰要因
const drag = 0.95;
function setup() {
createCanvas(700, 400);
velocityV = createVector(0, 0);
positionV = createVector(0, 0);
forceV = createVector(0, 0);
current = createVector(0, 0);
previous = createVector(0, 0);
}
function draw() {
background(155);
// マウスボタンが押されているなら
if (painting) {
// マウスの今の位置をメモ
current.x = mouseX;
current.y = mouseY;
// マウスの今の位置から前の位置を引いて、その差分を力と見なす
forceV = p5.Vector.sub(current, previous);
// マウスの前の位置をマウスの今の位置とする
previous.x = current.x;
previous.y = current.y;
}
// 毎フレーム移動させたいので、毎フレーム呼び出す
updateParticle(forceV);
drawParticle();
}
// マウスボタンがが押されたら
function mousePressed() {
// 描画状態にあるなら
painting = true;
// マウスの前の位置を、今のマウスの位置にする
// => current - previous = 0 になるので、forceVが0になり、円が静止する
previous.x = mouseX;
previous.y = mouseY;
// 前の速度が生きているのでリセットする
velocityV.mult(0);
// 円のスタート位置はマウスの位置
positionV.x = mouseX;
positionV.y = mouseY;
}
// 円の移動に関するベクトルを操作
function updateParticle(force) {
force.mult(0.05);
// 加速度は速度を変更する
velocityV.add(force);
// スローダウン
velocityV.mult(drag);
// 速度は位置を変更する
positionV.add(velocityV);
}
// 円の位置を反映しているpositionベクトルを使って円を描画
function drawParticle() {
ellipse(positionV.x, positionV.y, 10);
}
// マウスボタンが放されたら描画状態は終わり
function mouseReleased() {
painting = false;
}
変数のpositionVは円の位置を表すベクトルで、velocityVは円の速度を表すベクトルです。またここでは加速度ではなく、力のベクトルとしてforceVを使用していますが、扱いは加速度と同じです。
velocityV = createVector(0, 0);
positionV = createVector(0, 0);
forceV = createVector(0, 0);
current = createVector(0, 0);
previous = createVector(0, 0);
変数currentは今のフレームでのマウス位置を、previousは前のフレームでのマウス位置を表すベクトルです。draw()関数の初めで、currentにmouseXとmouseYを割り当て、最後でpreviousにcurrentを割り当てます。すると、その間でcurrentとpreviousの差が計算できます。これは変位に当たるもので、本来なら時間で割ります。しかし前の例のように後で小さくする調整を加えるので、ここでことさら計算を複雑にする必要はありません。
また時間で割らない場合、この変位(時間で割らない速度)は(時間で割らない)加速度として使用できます。そしてこの値は今の場合、止まっているものを動かすので、加速度というより力と見なす方がしっくりきます(加速度でも力でも行う計算に変わりはありません)。変数paintingはマウスプレスでtrueに、マウスリリースでfalseに設定し、次のコードをマウスボタンが押されている間だけ実行されるようにします。
if (painting) {
current.x = mouseX;
current.y = mouseY;
// マウスの今の位置から前の位置を引いて、その差分を力と見なす
forceV = p5.Vector.sub(current, previous);
previous.x = current.x;
previous.y = current.y;
}
円を、マウスリリース後も動かしたい場合には、円の移動に関する変数を更新するコードと円を描画するコードは、if (painting) {…} の外に出す必要があります。
// 毎フレーム移動させたいので、毎フレーム呼び出す
updateParticle(forceV);
drawParticle();
draw()関数からはupdateParticle()とdrawParticle()を呼び出しますが、必ず円の移動に関する変数を更新するupdateParticle()を呼び出してから円を描画する必要があります。updateParticle()には変位=>速度->加速度->力であるforceを渡し、「加速度は速度を変更する」と「速度は位置を変更する」を実行します。変数dragは速度を遅くする要因で、1未満の値を指定すと速度が遅くなります。
function updateParticle(force) {
force.mult(0.05);
velocityV.add(force);
// スローダウン
velocityV.mult(drag);
positionV.add(velocityV);
}
function drawParticle() {
ellipse(positionV.x, positionV.y, 10);
}
function mouseReleased() {
painting = false;
}
マウスプレス時に呼び出されるmousePressed()関数では、多くのことを行います。
function mousePressed() {
painting = true;
previous.x = mouseX;
previous.y = mouseY;
velocityV.mult(0);
positionV.x = mouseX;
positionV.y = mouseY;
}
previousにマウス位置を割り当てるのは、forceVをゼロにするためです。そして velocityV.mult(0)で、残っている速度もゼロにし、positionVにマウス位置を割り当てることで、静止した円がマウスのダウン位置に描画できます。
次は複数の円を描画し、それらを線でつなぎます。また描いた円と線は消えるようにします。
ドラッグで複数の円と線を描画、描いた円と線はやがてフェードアウトする
数値でも文字列でもオブジェクトでも、同種のものをまとめたい場合には、配列がよく使用されます。今の場合で言うと円で、正確に言うと、複数の円を描画するための位置と速度、力の情報です。前の例では、円を1つしか描いていなかったので、positionVやvelocityV、forceVで事足りましたが、複数の円を描く場合には、これらが1つの円につき1つずつ必要になります。
配列を使うと、たとえば[p1,v1,f1,p2,v2,f2,…,pn,vn,fn]のように、1つの円につきp5.Vectorオブジェクトを3つずつ、並べて保持することができます。ただしこの方法は、欲しい情報を抜き出すときに非常に不便です(JavaScriptのImageDataと同様)。
また[[p1,v1,f1],[p2,v2,f2],…,[pn,vn,fn]]のように、1つの円につきp5.Vectorオブジェクトを3つずつ配列に入れて、全体を1つの配列でくくる方法も考えられます。
さらに、JavaScriptの汎用オブジェクトを使った次の方法も考えられます。
これは、new Object() や{} で作成できるJavaScriptの汎用オブジェクトを円の数だけ作成し、それにそれぞれの円の位置や速度、力の値を属性(プロパティ)として持たせる方法です。属性を追加する必要ができた場合に容易に追加できるので、上の配列の入れ子の方法よりはるかに柔軟性があります。
具体的には次のように汎用オブジェクトを作成し、属性を設定します。
// particleObjectオブジェクトを作成し、速度や位置、力、寿命などの属性を設定して、
// particles配列に追加する
const particleObject = {};
particleObject.velocityV = createVector(0, 0);
particleObject.positionV = createVector(mouseX, mouseY);
particleObject.forceV = createVector(force.x, force.y);
particleObject.lifeSpan = 255;
particles.push(particleObject);
値は次のように設定します。
// particles配列を走査し、particleObjectの属性に値を設定する for (let i = 0; i < particles.length; i++) { const particleObject = particles[i]; particleObject.velocityV.mult(0); particleObject.velocityV.add(particleObject.forceV); particleObject.velocityV.mult(drag); particleObject.positionV.add(particleObject.velocityV); }
sketch.jsのコード全体は次のようになります。
// クリックした位置に円を描画、
// さらにクリックすると円は線でつながれる
// 円と線は時間経過とともにフェードアウトする
// ドラッグすると、その軌跡に合わせて円と線を描く
// ドラッグを終え、さらにクリックすると、最新の円も前の円と線で繋がれる
//let positionV;
//let velocityV;
//let forceV;
let current;
let previous;
let painting = false;
const drag = 0.05;
// particleObjectを入れる配列
const particles = [];
// 次のパーティクル(円)を作るまで待つ時間
let next = 0;
function setup() {
createCanvas(700, 400);
// 関係するベクトルを初期化
//velocityV = createVector(0, 0);
//positionV = createVector(0, 0);
//forceV = createVector(0, 0);
current = createVector(0, 0);
previous = createVector(0, 0);
}
function draw() {
background(155);
// next時間が経過し、かつマウスボタンが押されているなら
if (millis() > next && painting) {
current.x = mouseX;
current.y = mouseY;
let forceV = p5.Vector.sub(current, previous);
createParticles(forceV);
// next時間をランダムに設定
next = millis() + random(100);
previous.x = current.x;
previous.y = current.y;
}
updateParticles();
drawParticles();
}
// マウスボタンがが押されたら
function mousePressed() {
// next時間をリセット
next = 0;
painting = true;
previous.x = mouseX;
previous.y = mouseY;
//velocityV.mult(0);
//positionV.x = mouseX;
//positionV.y = mouseY;
}
function createParticles(force) {
force.mult(0.05);
// particleObjectオブジェクトを作成し、
// 速度や位置、力、寿命などの属性を設定して、
// 配列に追加する
const particleObject = {};
particleObject.velocityV = createVector(0, 0);
particleObject.positionV = createVector(mouseX, mouseY);
particleObject.forceV = createVector(force.x, force.y);
particleObject.lifeSpan = 255;
particles.push(particleObject);
}
// 円の移動に関するベクトルを操作
function updateParticles() {
//velocityV.add(force);
//velocityV.mult(drag);
//positionV.add(velocityV);
// particles配列を走査して、各particleObjectに移動に関するベクトルを与える
for (let i = 0; i < particles.length; i++) {
const particleObject = particles[i];
particleObject.velocityV.mult(0);
particleObject.velocityV.add(particleObject.forceV);
particleObject.velocityV.mult(drag);
particleObject.positionV.add(particleObject.velocityV);
}
}
// 円の位置を反映しているpositionベクトルを使って円を描画
function drawParticles() {
// 新しいものほど先に描画したいので、配列の後ろから処理する
// 古いもので寿命の尽きたものは配列から削除する
for (let i = particles.length - 1; i >= 0; i--) {
const particleObject = particles[i];
// 寿命を減らす
particleObject.lifeSpan--;
// 寿命が尽きたら配列から取り除き、描画対象からはずす
if (particleObject.lifeSpan <= 0) {
particles.splice(i, 1);
// 寿命がまだあれば描画
}
else {
const xpos = particles[i].positionV.x;
const ypos = particles[i].positionV.y;
stroke(0, particleObject.lifeSpan);
fill(0, particleObject.lifeSpan / 2);
ellipse(xpos, ypos, 10, 10);
// 配列の末尾以外なら線でつなぐ
if (i != particles.length - 1) {
line(xpos, ypos, particles[i + 1].positionV.x, particles[i + 1].positionV.y)
}
}
}
}
function mouseReleased() {
painting = false;
}
キャンバスをクリックすると黒丸が描画できます。ドラッグすると線でつながれた複数の黒丸が描画できます。黒丸と線は移動し、やがてフェードアウトします。
汎用オブジェクトのparticleObjectを作成しているのはcreateParticles()関数の中です。この関数は (millis() > next && painting) という条件が満たされているときにdraw()関数から呼び出されます。millis() > next というのは、next時間が経過したら、という意味です。変数nextは少し後の行で、next = millis() + random(100) によって設定されます。
ここでのポイントは、汎用オブジェクトを使うと、そのオブジェクト(particleObject)独自の値をそのオブジェクトで持ちつづけられるということです。たとえばvelocityV属性の値はどれも同じベクトル(0,0)ですが、positionV属性の値はそのオブジェクトが作成された時点でのマウス位置を表すベクトルです。このとき作成されたオブジェクトはそのマウス位置を自分の値として保持するので、その値を使って円がそのときのマウス位置に描画できます。
const particleObject = {};
particleObject.velocityV = createVector(0, 0);
particleObject.positionV = createVector(mouseX, mouseY); // このオブジェクト独自の値
particleObject.forceV = createVector(force.x, force.y); // このオブジェクト独自の値
particleObject.lifeSpan = 255;
particles.push(particleObject);
drawParticles()関数は残念ながら、複数の円を描き、線で結び、さらにフェードアウトさせるために、かなり複雑になっています。
function drawParticles() {
for (let i = particles.length - 1; i >= 0; i--) {
const particleObject = particles[i];
particleObject.lifeSpan--;
if (particleObject.lifeSpan <= 0) {
particles.splice(i, 1);
}
else {
const xpos = particles[i].positionV.x;
const ypos = particles[i].positionV.y;
stroke(0, particleObject.lifeSpan);
fill(0, particleObject.lifeSpan / 2);
ellipse(xpos, ypos, 10, 10);
if (i != particles.length - 1) {
line(xpos, ypos, particles[i + 1].positionV.x, particles[i + 1].positionV.y)
}
}
}
}
for (let i = particles.length - 1; i >= 0; i--) { で、配列の後ろの要素から参照しているのは、新しいもの(配列の後ろにあるparticleObject)ほど先に描画に使用するためです。
particleObjectのlifeSpanはそのオブジェクトの"寿命"に使用する属性で、drawParticles()が呼び出されるたびに、各particleObjectのlifeSpanの値が1ずつ減っていきます。この値を線の色と塗り色に利用すると、作成されてから時間のたった古いものほどより薄いグレーになるので、フェードアウトが表現できます。
この例では、汎用オブジェクトを使って描画する円の属性を保持しましたが、この方法を使おうと考えた時点が、Particleクラスを導入するタイミングかもしれません。
さらにこの例では、線は1回のドラッグで途切れず、いつまでも1本でつながっていきます。これはこれでひとつの仕様にも思えますが、サンプルとは異なります。サンプルのように線の接続が1回のドラッグで終わるようにするには、また別の工夫が必要になります。
1回のドラッグで線を1本にする
前の例は、言わばひと筆書きです。ひと筆書きでなく、1回のドラッグで1本の線と複数の円にするには、マウスプレス->ドラッグ->マウスリリースまでのマウスの経路(パス)をひとまとめにして、そのparticleObjectのまとまりを1本の線と複数の円として描画する必要があります。そのための方法として、particles配列の中にさらに配列を作成し、そこにパスごとのparticleObjectを入れることにします。
let current;
let previous;
let painting = false;
const drag = 0.05;
const particles = [];
let next = 0;
// マウスを押した回数
// particles配列に追加する配列の特定に使用する
let mousePressedCount = 0;
function setup() {
createCanvas(700, 400);
current = createVector(0, 0);
previous = createVector(0, 0);
}
function draw() {
background(155);
if (millis() > next && painting) {
current.x = mouseX;
current.y = mouseY;
let forceV = p5.Vector.sub(current, previous);
createParticles(forceV);
next = millis() + random(100);
previous.x = current.x;
previous.y = current.y;
}
updateParticles();
drawParticles();
}
function mousePressed() {
next = 0;
painting = true;
previous.x = mouseX;
previous.y = mouseY;
// particles配列のmousePressedCount番めの要素として
// 空の配列を追加
particles[mousePressedCount] = [];
}
function createParticles(force) {
force.mult(0.05);
const particleObject = {};
particleObject.velocityV = createVector(0, 0);
particleObject.positionV = createVector(mouseX, mouseY);
particleObject.forceV = createVector(force.x, force.y);
particleObject.lifeSpan = 255;
// particles.push(particleObject);
// このparticleObjectを追加したい配列を特定
// => 直近のタイミングでmousePressed()で作成した空の配列
const arr = particles[mousePressedCount];
// その配列にparticleObjectを追加
arr.push(particleObject);
}
function updateParticles() {
// 今度はparticles配列に含まれる各配列を走査する必要がある
for (let i = 0; i < particles.length; i++) {
// particles配列に含まれる各配列を特定
const arr = particles[i];
// その配列を走査
for (let j = 0; j < arr.length; j++) {
const particleObject = arr[j];
//const particleObject = particles[i];
particleObject.velocityV.mult(0);
particleObject.velocityV.add(particleObject.forceV);
particleObject.velocityV.mult(drag);
particleObject.positionV.add(particleObject.velocityV);
}
}
}
function drawParticles() {
// 今度はparticles配列に含まれる各配列を走査する必要がある
for (let j = 0; j < particles.length; j++) {
// particles配列に含まれる各配列を特定
const arr = particles[j];
// 走査対象がparticlesからarrに変わる
for (let i = arr.length - 1; i >= 0; i--) {
const particleObject = arr[i];
particleObject.lifeSpan--;
if (particleObject.lifeSpan <= 0) {
arr.splice(i, 1);
}
else {
const xpos = arr[i].positionV.x;
const ypos = arr[i].positionV.y;
stroke(0, particleObject.lifeSpan);
fill(0, particleObject.lifeSpan / 2);
ellipse(xpos, ypos, 10, 10);
if (i != arr.length - 1) {
line(xpos, ypos, arr[i + 1].positionV.x, arr[i + 1].positionV.y)
}
}
}
}
}
function mouseReleased() {
painting = false;
// マウスクリックの回数を1増やす
mousePressedCount++;
}
ここで作成しているparticles配列の中身の構造を図で示すと下のようになります。前の例ではparticlesに全部並べて入れていましたが、ここでは、1回のマウスのドラッグ中(マウスプレスからマウスリリースまで)に作成するparticleObjectを、mousePressedCount回めの配列に入れ、その配列をparticlesに入れています。
particles配列に追加する新しい配列は、mousePressed()関数の次のコードで作成しています。
particles[mousePressedCount] = [];
変数mousePressedCountは最初0で、マウスのリリース時に1だけ大きくします。particles[mousePressedCount] は、mousePressedCount回めに作成したparticles配列内の配列を指しています。mousePressedCountをインデックスとして使用することで作成した配列が後から参照できます。
function createParticles(force) {
...
const arr = particles[mousePressedCount];
arr.push(particleObject);
}
updateParticles()とdrawParticles()関数では、particles配列に含まれる複数の配列を参照する必要があるので、2重のforループを使っています。こうなってくると、事態がかなり複雑化していることが実感できます。このparticles配列に含まれる複数の配列を参照する働きを行っているのがPathクラスです。
まとめ
サンプルのコードと、ここまで見てきたsketch.jsのコードを見比べると、sketch.jsとParticle.js、Path.jsのコードは実にスマートで整然としていることが分かります。とは言え、繰り返しになりますが、誰でもが最初からサンプルの構造を思いつき、ParticleクラスとPathクラスを書き始められるわけではありません。
sketch.jsに機能を加えていくうちにだんだん複雑になっていき、スパゲッティコード化して手に負えなくなったときに、機能の分散化に進めばよいのです。慣れてくると、Particleクラス相当は思い付くようになるかもしれません。もしPathクラス相当が着想できたらかなりの上級者です。