0
0

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

Last updated at Posted at 2023-12-23

この記事は、「完走賞ゲットのため小ネタ 25記事を投稿しようとチャレンジ v2 Advent Calendar 2023」の 24日目の記事です。

今回の内容

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

公式サンプルについて

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

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

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

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 の座標計算をしているようです。

    v2.lerp(v1, 0.1);

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

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

  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() で求める形にします。また、その「特定の割合」という部分も計算する仕組みを作ります。

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

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);
}

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

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

書きかえ後 〜その2〜

もう少しだけ変更を加えてみます。

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

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

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;
  }
}

描画結果も掲載します。

以上です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0