p5.Vector を使った、引力・斥力を受けて動く点のアニメーション

2023年10月9日月曜日

p5.js アニメーション

t f B! P L

先日 createVector() のリファレンス記事を書きました。その中で作例コードを書いてみて、p5.Vector のメソッドをもっと使ってみたくなったので、こちらの記事でまた別の作例コードを書いてみます。

2つの点の間に働く力をベクトルで定義し、その力を受けて動くアニメーションを p5.js で書きます。


👉 Here is the English version of this article.

 

p5.Vector を使ったアニメーション

ということで、作ったものがこちらです。

赤い点は等速円運動をしています。黒い点は赤い点との距離が遠ければ引力、近ければ斥力を受けます。

これは重力ではありません。ここで定義しているのは架空の物理法則です。摩擦もなければ慣性も加速度もありません。さらに赤い点は黒い点からは力を受けません。現実の物理法則が通用しない、想像を絶した異世界の運動です。

引力と斥力の値は、マイナス値も含めた力の大きさになります。力が働く方向は、赤い点と黒い点を結ぶ線の方向で、引力なら黒い点から赤い点の方向、斥力ならその逆です。

ベクトルは力の大きさだけではなく、方向も持っていますので、p5.Vector を使うとこの架空の法則をすっきりコードに落とすことができます。

 

作例コード解説

先のアニメーションを描く p5.js のコードはこちらです。



/** 
 * p5.Vector を使った、引力・斥力を受けて動く点のアニメーション
 * 点 1個の場合
 * 
 * @author @deconbatch
 * @version 0.1
 * @license GPL3
 * p5.js 1.6.0
 * created 2023.02.15
 */

const w = 480;
const h = w;
const pullFix = 20;
const pushFix = 30;
const baseDist = w * 0.25;
const moverR = w * 0.2;
const moverT = 0.1;

let node;

function setup() {
  createCanvas(w, h);
  frameRate(24);
  initial();
}

function draw() {
  // 動く基準点の位置計算
  const mover = createVector(
    w * 0.5 + moverR * cos(moverT * frameCount),
    h * 0.5 + moverR * sin(moverT * frameCount)
  );

  // 力を受ける点の位置計算
  let d = (p5.Vector.dist(mover, node) - baseDist) / baseDist;
  let g = (d < 0) ? d * pushFix : d * pullFix;
  let t = p5.Vector.sub(mover, node).heading();
  node.add(
    g * cos(t),
    g * sin(t),
    -node.z + (d + 0.5) * w * 0.1
  );

  // 描画
  background(192);
  fill(192, 0, 0);
  circle(mover.x, mover.y, 10);

  fill(16);
  circle(node.x, node.y, node.z);

}

// 初期化
function initial() {
  frameCount = 0;
  node = createVector(
    random(w),
    random(h),
    0.0
  );
  noStroke();
}

// マウスクリックでやり直し
function mouseClicked() {
  initial();
}
  


変数解説

mover は、引力、斥力の基準となる等速円運動する赤い点です。



 const mover = createVector(
   w * 0.5 + moverR * cos(moverT * frameCount),
   h * 0.5 + moverR * sin(moverT * frameCount)
 );
  


これらは、mover の円運動の半径と回転速度を決める定数です。



 const moverR = w * 0.2;
 const moverT = 0.1;
  


node は、引力、斥力を受けて動く黒い点です。



 const pullFix = 20;
 const pushFix = 30;
 const baseDist = w * 0.25;


下記は、このコードの世界の物理定数と考えてください。値を変えると黒い点の挙動が変わります。



 const pullFix = 20;
 const pushFix = 30;
 const baseDist = w * 0.25;


 

力の計算

p5.Vector.dist() は 2つの p5.Vector 間の距離を算出してくれる、p5.Vector が持つメソッドです。

下記の式で、mover と node 間の距離が基準距離(baseDist)より近ければ斥力(d * pushFix)、遠ければ引力(d * pullFix)が g にセットされます。



 let d = (p5.Vector.dist(mover, node) - baseDist) / baseDist;
 let g = (d < 0) ? d * pushFix : d * pullFix;



重力じゃないのに g と名付けてしまった…

 

こちらは力が働く方向を算出する部分です。



 let t = p5.Vector.sub(mover, node).heading();


p5.Vector.sub() は 2つの p5.Vector の引き算をするメソッドで、この書き方だと mover から node を引き算した新たなベクトルを返します。返された値がベクトルということは、方向を持ちます。その方向は、node から見た mover の方向になります。

参考:ベクトルの引き算 | イメージングソリューション

p5.Vector.heading() はベクトルの方向を返してくれるメソッドで、上記の式により node から見た mover 方向の角度が t にセットされます。

 

描画

計算した力の大きさと方向分を、node の位置 x, y に加算します。



 node.add(
   g * cos(t),
   g * sin(t),



それはいいとして、z 部分のこれは何でしょう?



   -node.z + (d + 0.5) * w * 0.1


p5.Vector は三次元のベクトルを表現できるので、x, y, z の属性を持っているのですが、今回は二次元のベクトルとして扱い、z は黒い点の大きさの値を保持するために使いました。

 

なんだか三次元の円的書き方ですが、node.x, node.y が座標で node.z がサイズの普通の円なのです。



 circle(node.x, node.y, node.z);


いけませんね、こういう使い方は。コードを読む人に誤解を与えます。

 

遊ぶ

draw() 中の background(192) で毎回塗りつぶしていますが、これを initial() に移動させると、黒い点は不規則に動いてるように見えて、規則的な動きであることがわかります。
※撮影の都合上白い点になっています。


軌跡を残すことで、面白い形を描くことができました。


定数値を変えるとこんな形も。


 

ノードを 1個じゃなくて複数にしたら面白いかな?と思いましたが、2個や 3個ならいざ知らず、10個も 20個もになると全然面白くありませんでした。


数が多くなると、単に赤い点からの等距離で動いてるだけというのがバレてしまって興が削がれるみたいです。

 

締め

架空の物理法則を設定して、その法則がどんな動きを描き出すのか、想像するのも、作ってみるのも楽しいものです。

p5.Vector は運動や力を表現するのに適したベクトルを記述するのに便利なクラスです。ベクトルの計算に必要な数々のメソッドを備えており、物理法則を楽にコードに落とすことができます。

p5.Vector で独自の物理法則を持った世界を創造してみませんか?あなたも創造主になれる!

 

QooQ