この記事は、「完走賞ゲットのため小ネタ 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;
}
}
描画結果も掲載します。
以上です。