p5.js の公式サンプルの p5.Vector を使ったモーフィングにイージングを適用するための検討【完走賞ゲット-24】

:::note info
※ [Qiita の完走賞をゲットするための条件は「単一／複数のカレンダーに投稿した記事が合計25」です](https://twitter.com/Qiita/status/1593441648796524545)
:::

この記事は、「[完走賞ゲットのため小ネタ 25記事を投稿しようとチャレンジ v2 Advent Calendar 2023](https://qiita.com/advent-calendar/2023/youtoy)」の 24日目の記事です。

## 今回の内容
この記事は、p5.js の公式サンプルの p5.Vector を使ったモーフィングに、イージングを適用してみるための検討をするという内容です。

### 公式サンプルについて
p5.js の公式サンプルの 1つに以下のモーフィングのサンプルがあります。

●examples | p5.js
　https://p5js.org/examples/motion-morph.html

動きとしては、円と四角形の間で形状変化が起こるというものです。
このサンプルのコードは、具体的には以下のとおりです。

```javascript
let circle = [];
let square = [];

let morph = [];
let state = false;

function setup() {
  createCanvas(520, 400);

  for (let angle = 0; angle < 360; angle += 9) {
    let v = p5.Vector.fromAngle(radians(angle - 135));
    v.mult(100);
    circle.push(v);
    morph.push(createVector());
  }

  for (let x = -50; x < 50; x += 10) {
    square.push(createVector(x, -50));
  }
  for (let y = -50; y < 50; y += 10) {
    square.push(createVector(50, y));
  }
  for (let x = 50; x > -50; x -= 10) {
    square.push(createVector(x, 50));
  }
  for (let y = 50; y > -50; y -= 10) {
    square.push(createVector(-50, y));
  }
}

function draw() {
  background(51);

  let totalDistance = 0;

  for (let i = 0; i < circle.length; i++) {
    let v1;
    if (state) {
      v1 = circle[i];
    } else {
      v1 = square[i];
    }
    let v2 = morph[i];
    v2.lerp(v1, 0.1);
    totalDistance += p5.Vector.dist(v1, v2);
  }

  if (totalDistance < 0.1) {
    state = !state;
  }

  translate(width / 2, height / 2);
  strokeWeight(4);
  beginShape();
  noFill();
  stroke(255);

  morph.forEach((v) => {
    vertex(v.x, v.y);
  });
  endShape(CLOSE);
}
```

### 公式サンプルで動きを作っている部分
変化を生み出している部分を見ていきます。

移動する点と、目標となる地点（円か四角形のどちらかの上にある特定の座標）との間で、以下の線形補間を用いた x・y の座標計算をしているようです。

```javascript
    v2.lerp(v1, 0.1);
```

`v2 = morph[i]` となっているので、上記の処理で morph[i] の値が上書きされて変化していきます。

上記の処理の目標となる地点は、円・四角のどちらにするかを切り替える必要もあります。それについては、以下の部分で判定・切り替えを行っているようです。

```javascript
  let totalDistance = 0;

  for (let i = 0; i < circle.length; i++) {
    ...
    totalDistance += p5.Vector.dist(v1, v2);
  }

  if (totalDistance < 0.1) {
    state = !state;
  }
```

図形を描画している各点の座標と、目標地点との座標の距離の総和を計算し、それが特定の閾値より小さくなったら、目標地点を切り替える処理をしていました。

### 書きかえ後
それでは、イージングを適用したものを作ってみます。
元のサンプルを書きかえて、square と circle の x・y座標間の特定の割合の位置の座標を lerp() で求める形にします。また、その「特定の割合」という部分も計算する仕組みを作ります。

具体的には、以下の処理を作りました。

```javascript
let circle = [];
let square = [];

let morph = [];
let state = false;

let speed = 0.01;
let ratio = 0;

function setup() {
  createCanvas(520, 400);

  for (let angle = 0; angle < 360; angle += 9) {
    let v = p5.Vector.fromAngle(radians(angle - 135));
    v.mult(100);
    circle.push(v);
    morph.push(createVector());
  }

  for (let x = -50; x < 50; x += 10) {
    square.push(createVector(x, -50));
  }
  for (let y = -50; y < 50; y += 10) {
    square.push(createVector(50, y));
  }
  for (let x = 50; x > -50; x -= 10) {
    square.push(createVector(x, 50));
  }
  for (let y = 50; y > -50; y -= 10) {
    square.push(createVector(-50, y));
  }
}

function draw() {
  background(51);

  let v1, v2;
  for (let i = 0; i < circle.length; i++) {
    if (state) {
      v1 = circle[i];
      v2 = square[i];
    } else {
      v1 = square[i];
      v2 = circle[i];
    }
    morph[i] = p5.Vector.lerp(v1, v2, easing(ratio));
  }

  translate(width / 2, height / 2);
  strokeWeight(4);
  beginShape();
  noFill();
  stroke(255);

  morph.forEach((v) => {
    vertex(v.x, v.y);
  });
  endShape(CLOSE);

  ratio += speed;
  if (ratio >= 1) {
    ratio = 0;
    state = !state;
  }
}

function easing(t) {
  return 1 - pow(1 - t, 4);
}
```

これで、モーフィングにイージングを適用することができました。
今回のイージングの実装には、以下の記事でも使った「[イージング関数チートシート](https://easings.net/ja)」を活用しました。

●「イージング関数チートシートの関数」「p5.js の lerp()、frameCount」を組み合わせたイージングの繰り返しの実装【完走賞ゲット-18】 - Qiita
　https://qiita.com/youtoy/items/06d4ef113175463ed4d7

### 書きかえ後　〜その2〜
もう少しだけ変更を加えてみます。

主な変更点は 2つで、「frameCount を使ったイージングにする」「イージングを弾む動きにする」という内容です。

その変更を加えた後のプログラム全体は、以下のとおりです。

```javascript
let circle = [];
let square = [];

let morph = [];
let state = false;

const size = 70;

function setup() {
  createCanvas(520, 400);

  for (let angle = 0; angle < 360; angle += 9) {
    let v = p5.Vector.fromAngle(radians(angle - 135));
    v.mult(150);
    circle.push(v);
    morph.push(createVector());
  }

  for (let x = -size; x < size; x += size / 5) {
    square.push(createVector(x, -size));
  }
  for (let y = -size; y < size; y += size / 5) {
    square.push(createVector(size, y));
  }
  for (let x = size; x > -size; x -= size / 5) {
    square.push(createVector(x, size));
  }
  for (let y = size; y > -size; y -= size / 5) {
    square.push(createVector(-size, y));
  }
}

function draw() {
  background(51);

  const cycleLength = 120;
  const normalizedTime = norm(frameCount % cycleLength, 0, cycleLength - 1);
  if (normalizedTime === 0) {
    state = !state;
  }

  let v1, v2;
  for (let i = 0; i < circle.length; i++) {
    if (state) {
      v1 = circle[i];
      v2 = square[i];
    } else {
      v1 = square[i];
      v2 = circle[i];
    }
    morph[i] = p5.Vector.lerp(v1, v2, easing(normalizedTime));
  }

  translate(width / 2, height / 2);
  strokeWeight(4);
  beginShape();
  noFill();
  stroke(255);

  morph.forEach((v) => {
    vertex(v.x, v.y);
  });
  endShape(CLOSE);
}

function easing(t) {
  return easeOutBounce(t);
  // return 1 - easeOutBounce(1 - t);
}

function easeOutBounce(t) {
  const n1 = 7.5625;
  const d1 = 2.75;

  if (t < 1 / d1) {
    return n1 * t * t;
  } else if (t < 2 / d1) {
    return n1 * (t -= 1.5 / d1) * t + 0.75;
  } else if (t < 2.5 / d1) {
    return n1 * (t -= 2.25 / d1) * t + 0.9375;
  } else {
    return n1 * (t -= 2.625 / d1) * t + 0.984375;
  }
}
```

描画結果も掲載します。

<iframe width="560" height="315" src="https://www.youtube.com/embed/LqHOKDsA5Q4?si=ENpvU5MFgka0qiDf" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

以上です。