この記事は、ニフティグループ Advent Calendar 2021 6日目の記事です。遅くなりすみません!
最近はシミュレーションの描画にハマっており、p5.js 触ってみたいなということで今回のネタにしました。
Vue.js のコンテナ内で、CompositionAPI の記法で p5.js を動かしたので紹介します。
CompositionAPI についての説明は割愛します。以下の記事を参考にさせていただきました。
作ったもの
振動するボールを作って動かすことができます。
マウスをクリックするとその位置にボールが集まってきます。
丸い物体が絶妙に揺れながら近づいてくるの可愛すぎる。
p5.js の紹介
p5.js はクリエイティブコーディングのための JavaScript のライブラリです。
アートやシミュレーションの描画として利用できます。
ここでは基本的な使い方を紹介します。
- setup(): 最初に呼び出され、canvas の大きさや背景色を指定
- draw(): 1/60秒ごとに呼び出される
- その他、円状の物体を表示する関数や、色を指定する関数、マウスの位置や状態を取得する関数が揃っている
例えば、 canvasを生成し、マウスがクリックされるとそのマウスの位置に円を描画するコードはこちら。
function setup() {
createCanvas(300, 300);
background(0);
}
function draw() {
// 1/60秒に一回、canvas の中心に直径50の円を描画
ellipse(width/2, height/2, 50, 50);
// マウスがクリックされていれば、drawCircleAtMouse 関数を実行
if (mouseIsPressed) {
drawCircleAtMouse();
}
}
function drawCircleAtMouse() {
// マウスの位置に直径 5 の円を描画
ellipse(mouseX, mouseY, 20, 20);
}
コンテナの作成
ディレクトリ構成
├── Dockerfile
├── docker-compose.yml
Dockerfile
- 今回はvue-cli でプロジェクトを作成
FROM node:16.3-alpine
WORKDIR /usr/src/app
RUN apk update && apk add curl && apk add vim
RUN npm install -g npm && npm install -g @vue/cli
docker-compose.yml
version: '3.8'
services:
p5_vue_container:
build: .
tty: true
volumes:
- ./:/usr/src/app
ports:
- "8080:8080"
コンテナを作成して中に入る
$ docker compose up -d
$ docker compose exec p5_vue_container sh
/usr/src/app #
プロジェクト作成
- 今回は Vue.js で p5.js を使えれば良いので、preset は Default (Vue 3) を選択
/usr/src/app # vue create p5-test
Vue CLI v4.5.15
? Please pick a preset: Default (Vue 3) ([Vue 3] babel, eslint)
? Pick the package manager to use when installing dependencies: NPM
以下のファイルを追加
- p5-test/js/brown.js
- p5-test/js/class/ballClass.js
最終的な主なディレクトリ構造
├── Dockerfile
├── docker-compose.yml
├── p5-test
├── src
├── App.vue
├── js
├── SetupP5.js
├── class
├── ballClass.js
Vue.js コンテナ内で p5.js を使う
こちらを参考にさせていただきました。
コンテナ内で p5.js をインストール
/usr/src/app # cd p5-test
/usr/src/app/p5-test # npm install --save p5
利用したいファイル上で p5 をインポート
- 今回はvue ファイル上でインポート
import p5 from 'p5';
vue ファイルの作成
App.vue を以下のコードに置き換える
<template>
<div>
<h1>Create Canvas</h1>
<!--ボール作成ボタン-->
<button v-on:click="creatBalls">Create Balls</button>
<!--全ボール削除ボタン-->
<button v-on:click="clearCanvas">Clear Canvas</button>
</div>
<br>
<!--canvas を配置する位置-->
<div id="canvas"></div>
</template>
<script>
import { onMounted, ref } from 'vue';
import p5 from 'p5';
import { p5Setup, addBalls, clearBalls } from './js/SetupP5';
export default {
setup() {
const P5 = ref();
// マウント時に canvas を生成
onMounted(() => {
P5.value = new p5(p5Setup);
});
// ボールを追加
const creatBalls = () => {
addBalls();
};
// ボールを全て削除
const clearCanvas = () => {
clearBalls();
};
return {
P5,
creatBalls,
clearCanvas
};
}
};
</script>
- ボールを作るボタンとボールを全て削除するボタンを用意
- canvas を特定の位置に出すためのタグを用意
- p5.js のインスタンスモードを利用するため、setup() で p5 変数を ref() で宣言
- 参考: [vue.jsの instance mode でp5.jsを動かす(メモ)]
(https://qiita.com/PmanRabbit/items/e5253249df108c904640) - 参考: p5.js を instance mode で使う
- 参考: [vue.jsの instance mode でp5.jsを動かす(メモ)]
- CompositionAPI では、基本的に setup() 内に関数などをまとめて記述できる
- setup() 内の onMounted で、マウント時に p5.js のインスタンスを作成
- ボール追加・削除ボタンを押したときに発火する関数も setup() に記述
p5.js のファイルを作成
p5.js でボールを描画するファイルと、ボールをオブジェクトとして管理するための Ball class の二つのファイルを作成
p5.js ファイル (SetupP5.js)
import { Ball } from './class/ballClass';
// 生成されるボールの数
const numBalls = 2;
// canvas の背景色
const bgcol = 25;
// ボールクラスを保存する配列
let ballArr = [];
// ボール追加フラグ
let addBallsFlag = false;
// ボール削除フラグ
let clearBallsFlag = false;
// vue ファイルで p5 インスタンスに渡す関数
const p5Setup = function(p5) {
// はじめに呼ばれる
p5.setup = () => {
// canvas 生成
const canvas = p5.createCanvas(500, 500);
// <div id="canvas"> に canvas を配置
canvas.parent('canvas');
// canvas の背景色
p5.background(bgcol);
// canvas 内の動きをなめらかにする
p5.smooth();
// draw()を 1/30秒ごとに実行
p5.frameRate(30);
};
// 1/frameRate 秒ごとに呼ばれる
p5.draw = () => {
p5.background(bgcol);
for (let i=0; i<ballArr.length; i++) {
const thisBall = ballArr[i];
// ボールの位置を計算
thisBall.update(p5)
// マウスが押されていれば、マウス方向へボールを移動
if (p5.mouseIsPressed) {
thisBall.agg(p5, p5.mouseX, p5.mouseY);
}
}
// ボールを追加
if (addBallsFlag) {
for (let i=0; i<numBalls; i++) {
const thisBall = new Ball(p5);
thisBall.drawBall(p5);
ballArr.push(thisBall);
}
addBallsFlag = false;
}
// ボールを全て削除
if (clearBallsFlag) {
ballArr = [];
clearBallsFlag = false;
}
};
};
// ボール追加フラグの変更
const addBalls = () => {
addBallsFlag = true;
}
// ボール削除フラグの変更
const clearBalls = () => {
clearBallsFlag = true;
}
export { p5Setup, addBalls, clearBalls };
- p5Setup 関数内に、p5 関連の処理を全て記述し、vue ファイルの p5 インスタンスに渡す
- ボールの追加・削除は、vue 側からフラグを変更する関数を呼び、フラグをもとに p5Setup() 内でボールの追加・削除を実行
Ball class
export class Ball {
constructor (p5) {
// ボールが描画される座標
this.x = p5.random(p5.width);
this.y = p5.random(p5.height);
// 描画されるボールの半径、色、色の透明度
this.radius = p5.random(10) + 10;
this.fillcol = p5.color(p5.random(255), p5.random(255), p5.random(255));
this.alph = 0;
}
// ボールを描画
drawBall(p5) {
// ボールの輪郭の線を消す
p5.noStroke();
// 色を設定
p5.fill(this.fillcol, this.alph);
// (x, y) に半径 radius の円を描画
p5.ellipse(this.x, this.y, this.radius*2, this.radius*2);
}
// ボールの位置を更新
update(p5) {
this.x += p5.random(-3, 3);
this.y += p5.random(-3, 3);
// 境界条件
if (this.x > (p5.width + this.radius)) {
this.x = 0 - this.radius;
}
if (this.x < (0 - this.radius)) {
this.x = p5.width + this.radius;
}
if (this.y > (p5.height + this.radius)) {
this.y = 0 - this.radius;
}
if (this.y < (0 - this.radius)) {
this.y = p5.height + this.radius;
}
// 更新した座標で再描画
this.drawBall(p5);
}
// マウスのクリック位置に集まる
agg(p5, mouseX, mouseY) {
const v = 0.02;
// ボールとマウスの位置を計算
let d = p5.dist(this.x, this.y, mouseX, mouseY)
// 距離が 2 より大きければ、マウスに近づくように座標を更新
if (d>2) {
this.x += v*(mouseX-this.x) + p5.random(-10, 10);
this.y += v*(mouseY-this.y) + p5.random(-10, 10);
}
// 更新した座標で再描画
this.drawBall(p5);
}
}
- ボール一つ一つをオブジェクトとして管理
- ボールの位置はランダムに -3 から 3 の範囲で動く
- マウスクリック時、マウス方向へ v の大きさ + -10 から 10 の範囲でランダムに移動
- 境界条件は端に来たら逆の端に移動する
- 某 RPG と同じ
動かす
プロジェクトのルートで実行
/usr/src/app/p5-test # npm run serve
localhost: にアクセス
まとめ
今回は Vue.js のコンテナ内で、CompositionAPI の記法で p5.js を動かしてみました。
ほとんど CompositionAPI の旨みは使っていないですが、Vue.js で p5.js を動かせたので良しとします。
ボール同士の衝突や複数の相互作用を入れたり、他のモデルを実装して遊んでみようと思います。