p5.jsは、JavaScriptを使ってクリエイティブコーディングを実現するライブラリです
Processingという言語をベースにしていて、プログラミング初心者にも扱いやすい設計がされています
p5.jsはCanvas APIを抽象化していて便利な関数が多く、使いやすいAPIが提供されているので、ネイティブのCanvasAPIを使うよりかなり楽です
備考
- Canvas APIは2Dグラフィックス用で、シンプルで使いやすく、基本的な図形やアニメーションに向いています
- WebGLは2D描画もできますが、3Dグラフィックス用で、グラフィックスカードを直接利用するため、複雑な3Dシーンや大量のオブジェクトを描画する場合でも高いパフォーマンスを発揮します
*p5.jsはcreateCanvas()関数にWEBGLオプションを指定することで、WebGLモードを有効にすることもできます
基本の使い方
/* CDN */
<script src="https://cdn.jsdelivr.net/npm/p5@1.5.0/lib/p5.js"></script>
<script>
function setup() {
createCanvas(300, 100);
frameRate(1);
}
function draw() {
console.log(frameCount) //frameCountは表示されたフレームの数
}
</script>
p5.jsの主な関数
createCanvas()
キャンバス要素を作成します
setup() の開始時に一度だけ呼び出す
createCanvas() を使用しない場合、100×100ピクセルですframeRate(Number型)
setup() 内でフレームレートを設定が推奨
frameRateを書かない場合はほとんどのコンピューターでは毎秒60フレーム
引数なしで frameRate() を呼び出すと現在のフレームレートが返されます
*滑らかなアニメーションには、毎秒24フレーム以上
指定された親に要素をアタッチする場合
<div id="myContainer"></div>
<script>
function setup() {
let cnv = createCanvas(100, 100);
cnv.parent('myContainer');
}
</script>
preload()
画像のロードなどで使用preload()
が定義されている場合、setup()
はその中のロード呼び出しが完了するまで待機します
*loadImage、loadJSON、loadFont、loadStrings以外はpreload()に含めない
*ちなみにすべてのロード関数はオプションの最後の引数としてコールバック関数を受け入れることもできますsetup()
プログラムの開始時に1回呼び出されます
画面サイズや背景色などの初期環境を定義しますdraw()
draw
関数はプログラムが停止するかnoLoop()
が呼び出されるまでコードを継続的に実行しますnoLoop()
・loop()
・redraw()
で制御しますdraw()
の実行回数はframeRate()
で制御できます
*setup()
でnoLoop()
が呼び出された場合でも、停止する前にdraw()
が1回実行されますnoLoop()
draw()
内のコードを継続的に実行するのを停止しますloop()
停止を再開できます
*setup()
内からloop()
を呼び出さないredraw()
draw()
内のコードを1回実行します
たとえばmousePressed() など登録されたイベントが発生した場合のみウィンドウを更新できます
描画の回数は引数に整数を追加します(デフォルトは1)isLooping()
isLooping() は現在の状態を返しますremove()
p5.js によって作成されたキャンバスとすべての要素が削除されますpush()
とpop()
push()
関数は現在の描画スタイルの設定と変換を保存し、pop()
はこれらの設定を復元します
*常に一緒に使用されます
デフォルトモード(グローバルモード)とインスタンスモード
- デフォルトモード(グローバルモード)
p5.jsの関数(setup や draw など)はグローバル名前空間に存在します
これは1つのスケッチには便利ですが、1つのページに複数のスケッチを含める場合には問題が生じます - インスタンスモード
スケッチはオブジェクトとして作成され、グローバル名前空間を汚染しません
これにより、複数のスケッチを1つのページに含めることができます
インスタンスモードの使い方
複数のp5スケッチを含める場合
//スケッチ関数を定義
const canvas1 = c1 => {
c1.setup = ()=> {
c1.createCanvas(100, 100).parent('canvas1');
c1.textSize(30);
c1.textAlign(c1.CENTER, c1.CENTER);
c1.noLoop();
};
c1.draw = () =>{
c1.background(200);
c1.text(c1.frameCount, c1.width / 2, c1.height / 2);
};
c1.mousePressed=()=>{
c1.redraw();
return false;
}
};
//p5インスタンスを作成
new p5(canvas1);
その他、基本的なこと
*mousePressed()などのイベントを使うと、なぜかスマフォでスクロールしなくなる😂ので
このページの実際のコードではクリックイベントを使用しています
マウスイベント、キーボードイベント、タッチイベント用の関数が用意されています
<div id="canvas1"></div>
<script>
function setup() {
createCanvas(100, 100).parent('canvas1');
frameRate(1);
textSize(30);
textAlign(CENTER, CENTER);
}
function draw() {
background(200);
text(frameCount, width / 2, height / 2);
}
function mousePressed() {
redraw();
return false;
}
/* 備考:touchStarted()やmousePressed()だとスマフォでスクロールしなくなるので
const el1 = document.getElementById('canvas1');
el1.addEventListener('click', ()=>{
redraw();
})
にしている*/
</script>
備考mousePressed()
関数について
マウスボタンが押されたときに呼び出されますtouchStarted()
が定義されていない場合代わりに mousePressed()
が定義されていれば呼び出されます
デフォルトの動作を防ぐには、メソッドの最後にreturn falseを追加します
windowResized()
はウィンドウのサイズが変更されるたびに1回呼び出されますresizeCanvas()
は指定された幅と高さにキャンバスのサイズを変更・クリア・再レンダリングをします
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
}
定数と便利な変数
- QUARTER_PI:円の円周と直径の比率の 4 分の 1 です
- HALF_PI:円の円周と直径の比率の半分
- PI:円の直径に対する円周の比率
- TWO_PI:円の円周と直径の比率の 2 倍
- windowWidth:ウィンドウの幅
- windowHeight:ウィンドウの高さ
- width:キャンバスの幅
- height:キャンバスの高さ
- mouseX:キャンバスの (0, 0) に対するマウスの現在の水平位置
- mouseY:キャンバスの (0, 0) に対するマウスの現在の垂直位置
setup内で設定するモード
- createCanvas: キャンバスのサイズを設定。
- colorMode: 色の指定方法を設定(RGBやHSB)
デフォルトはcolorMode(RGB)
colorMode(HSB) を設定するとHSBを使用でき、ます同系色や明暗をランダムに作りたいときに便利です - angleMode: 角度の単位を設定(ラジアンや度)
デフォルトはRADIANS
RADIANS or DEGREESを設定します - blendMode: 描画時のブレンドモードを設定
- rectMode: 矩形の描画基準を設定
- ellipseMode: 楕円の描画基準を設定
- imageMode: 画像の描画基準を設定
備考:弧度法と度数法
- RADIANS(弧度法)は円の半径と同じ長さの孤を切り取るときの角の大きさを「1 rad」と規定して、DEGREESより正確です
- DEGREES(度数法)は、円周を360等分したときの角の大きさを「1 deg」と規定
*「度数法」を使用するには
radians()(度をラジアンに変換する関数)を使用するか
angleMode(DEGREES)を使用します
ベクトル
ベクトル(数字のリスト)で2次元「x,y」や3次元「x,y,z」を管理すると便利createVector()
ベクトルを格納するためのデータ型を作成します
let v1;
function setup() {
createCanvas(100, 100);
stroke(255, 0, 255);
v1 = createVector(width / 2, height / 2);
}
function draw() {
background(255);
line(v1.x, v1.y, mouseX, mouseY); //v1.xとv1.yで取得
}
主なベクトル計算のメソッド
add()
ベクトルを加算sub()
ベクトルを減算mult()
スカラーを乗算div()
ベクトルをスカラーで除算mag()
ベクトルの長さを計算し、結果をfloatとして返します
*これは単に方程式 sqrt(xx + yy + z*z) と同じnormalize()
ベクトルを長さ「1」に正規化します
正規化はベクトルの方向は維持しつつ長さ「1」にすることです
*長さ「1」のベクトルのことを単位ベクトルと呼びます
扱いたいオブジェクトのクラスを定義
p5.jsでは、setup関数とdraw関数を使ってスケッチを作成しますが、オブジェクト指向のアプローチを取り入れることで、コードをより整理しやすく、再利用可能にする
- 扱いたいオブジェクトの状態(プロパティ)と動作(メソッド)をまとめたものを作成
- setup関数で初期化
オブジェクトのインスタンスを作成、これにより、オブジェクトの初期状態を設定し、必要な準備を行う - draw関数での更新と描画
draw関数内で、オブジェクトの状態を更新し描画します
これにより、毎フレームごとにオブジェクトの動きを管理し、表示します
クリックでスタートとストップ
<div id="canvas2"></div>
<script>
const NUM = 30
let deg = 360/ NUM
let balls = [];
function setup() {
createCanvas(300, 300).parent('canvas2');
colorMode(HSB);
angleMode(DEGREES)
for (let i = 0; i < NUM; i++) {
balls.push(new Ball(360/ NUM * i, cos(deg * i), sin(deg * i)))
}
noLoop();
}
function draw() {
background(255);
balls.forEach(ball=>{
ball.move();
ball.display();
})
}
//クリックで停止と再開
function mousePressed() {
isLooping()? noLoop():loop()
return false;
}
class Ball {
constructor(color, vX, vY) {
this.vPos = createVector(width/2, height/2);
this.radius = 15;
this.color = color;
this.vVelocity = createVector(vX, vY);
}
move() {
//ベクトルの加算するだけで、それぞれのXとYを加算する必要がない
this.vPos.add(this.vVelocity)
if(this.vPos.x > width || this.vPos.x < 0){
this.vVelocity.x = -this.vVelocity.x
}
if(this.vPos.y > height || this.vPos.y < 0){
this.vVelocity.y = -this.vVelocity.y
}
}
display() {
ellipse(this.vPos.x, this.vPos.y, this.radius, this.radius);
fill(this.color, 100, 100, 1);
noStroke()
}
}
</script>
cos()
コサインを計算(値は -1〜1の範囲で返されます)sin()
サインを計算(値は -1〜1の範囲で返されます)random()
- ランダムな浮動小数点数を返します
- 引数が指定されていない場合は、0から1までの乱数 (1 を含まない)
- 引数が1つの場合は、0からその数値までの乱数(数値は含まない)
- 引数が2つの場合は、最初の引数から2 番目の引数までの乱数(2 番目の引数は含まない)
- 配列の場合、その配列からランダムな要素を返します
noise()
指定された座標でのパーリンノイズ値を返します
0.005 ~ 0.03 のステップが最適ですatan2(y, x)
*第一引数がy座標
正のx軸から測定した、指定した点から座標原点までの角度 を計算します
angleMode() が RADIANS の場合は値は PI から -PIの範囲のfloat
angleMode() が DEGREES の場合は180から-180の範囲で返されます
<div id="canvas3"></div>
<script>
let eyes=[];
function setup() {
createCanvas(300, 300).parent('canvas3');
noStroke();
eyes.push(new Eye(c3.width*0.25, c3.height*0.25, 100));
eyes.push(new Eye(c3.width*0.25, c3.height*0.75, 100));
eyes.push(new Eye(c3.width*0.75, c3.height*0.75, 100));
eyes.push(new Eye(c3.width*0.75, c3.height*0.25, 100));
}
function draw() {
background(102);
eyes.forEach(eye=>{
eye.update(mouseX, mouseY);
eye.display();
})
}
class Eye{
constructor(x, y, size){
this.x = x;
this.y = y;
this.size = size;
this.angle = 0;
}
update(mx, my){
this.angle = atan2(my - this.y, mx - this.x);
}
display() {
push(); // 現在の描画状態を保存
translate(x, y); // 座標系を (x, y) に移動
fill(255); // 白で塗りつぶし
ellipse(0, 0, size, size); // 中心に大きな円を描画
rotate(angle); // 指定した角度だけ回転
fill(153, 204, 0); // 緑で塗りつぶし
ellipse(size / 4, 0, size / 2, size / 2); // 小さな円を描画
pop(); // 描画状態を復元
};
}
</script>
push()とpop()
push()は現在の描画状態(座標系、塗りの色など)を保存し
pop()は保存した描画状態を復元します
これにより、push()とpop()の間で行われた変換(例えば、translateやrotate)は他の描画に影響を与えません
Vertex(頂点)
vertex()
beginShape() および endShape() 関数内で点・線・多角形などの頂点座標を指定するために使用しますbeginShape()
beginShape()
はシェイプの頂点の記録を開始し、endShape()
は記録を停止します
* translate()、rotate()、scale() などの変換は、beginShape() 内では機能しません
*beginShape() 内で ellipse() や rect() などの他の形状を使用できません
パラメータの値- なし:形状は任意の不規則な多角形にすることができます
- POINTS: 一連の点を描く
- LINES: 一連の接続されていない線分 (個々の線))を描画します
- TRIANGLES :一連の個別の三角形を描画します
- TRIANGLE_FAN :最初の頂点を共有する一連の接続された三角形を扇状に描画します
- TRIANGLE_STRIP :一連の接続された三角形をストリップ形式で描画します
- QUADS: 一連の個別のクワッドを描画します
- QUAD_STRIP :隣接するエッジを使用してクワッド ストリップを描画し、次のクワッドを形成します
endShape()
beginShape()
の後にのみ呼び出すことができます
前回のbeginShape()
の呼び出し以降に定義されたすべての画像データが画像バッファーに書き込まれます
パラメータの値- CLOSE:形状を閉じる (開始と終了を接続する)
備考
極座標(距離,角度)と直交座標(x,y)
距離と角度からXとYを計算する
x = 距離 * cos(角度)
y = 距離 * sin(角度)
<div id="canvas4"></div>
<script>
function setup() {
createCanvas(300, 300).parent('canvas4');
push();
translate(width * 0.25, height * 0.25)
star(50, 20, 5);
pop();
push();
translate(width * 0.75, height * 0.25);
star(50, 20, 10);
pop();
push();
translate(width * 0.25, height * 0.75);
star(50, 50, 4);
pop();
push();
translate(width * 0.75, height * 0.75);
star(50, 40, 8);
pop();
}
function star(radius1, radius2, points) {
let angle = TWO_PI / points; // TWO_PIは360
let halfAngle = angle / 2.0;
beginShape();
for (let a = 0; a < TWO_PI; a += angle) {
let sx = cos(a) * radius2;
let sy = sin(a) * radius2;
vertex(sx, sy);
sx = cos(a + halfAngle) * radius1;
sy = sin(a + halfAngle) * radius1;
vertex(sx, sy);
}
endShape(CLOSE);
}
</script>
極座標で r(原点からの距離)= sin(n*角度) の「nを分数」にするとバラ曲線が描けます
バラ曲線 の表(nが分子・dが分母)
上の表8/5
<div id="canvas5"></div>
<script>
function setup(){
createCanvas(300, 300).parent('canvas5');
translate(width / 2, height / 2);
roseCurve(8,5);
}
function roseCurve(n,d) {
beginShape();
for(let angle = 0; angle < TWO_PI * d; angle += 0.01){
let r = 100 * sin(angle * (n / d));
//直座標(x,y)に変換
let x = r * cos(angle);
let y = r * sin(angle);
vertex(x, y);
}
endShape(CLOSE);
}
</script>
桜の花びら
<div id="canvas6"></div>
<script>
function setup(){
createCanvas(300, 300).parent('canvas6');
translate(width / 2, height / 2);
sakura(4)//花びらの枚数
}
function sakura(n) {
beginShape();
for(let angle = 0; angle < 360 ; angle++){
let A = n/ PI * radians(angle);
let mod = floor(A) % 2;
let r0 = pow(-1, mod) * (A - floor(A)) + mod;
let h = r0 < 0.8 ? 0 : 0.8 - r0
let r = r0 + 2 * h;
let x = 100 * r * cos(radians(angle));
let y = 100 * r * sin(radians(angle));
vertex(x, y);
}
endShape(CLOSE);
}
</script>
花びらが散るエフェクト
コードを見る
<div id="sakura"></div>
<div class="target-class"></div>
<script src="https://cdn.jsdelivr.net/npm/p5@1.5.0/lib/p5.js"></script>
<script>
const target = document.querySelector('.target-class');
const callback = function(entries, observer) {
entries.forEach(entry => {
if (!entry.isIntersecting) {
document.body.classList.add('hidden-class');
noLoop();
} else {
document.body.classList.remove('hidden-class');
loop();
}
});
};
const observer = new IntersectionObserver(callback);
observer.observe(target);
const sakura = (p) => {
let petals = [];
const NUM = 150;
class Sakura {
constructor() {
this.hue = p.random(320, 350);
this.alpha = p.random(0.6, 1);
this.size = p.random(20, 40);
this.xRadius = p.random(10, 50);
this.base = p.random(p.width);
this.vPos = p.createVector(this.base, p.random(p.height));
this.vAngle = p.createVector(p.random(360), p.random(360));
this.vVelocity = p.createVector(1, this.size / 20);
//花びらの形を変える
this.yScale = 1;
}
move() {
this.vAngle.add(this.vVelocity);
this.yScale = p.abs(p.sin(p.radians(this.vAngle.x)));
// X軸の動き 基準値 + 半径 * -1から1
this.vPos.x = this.base + this.xRadius * p.sin(p.radians(this.vAngle.y));
// Y軸の動き
this.vPos.y += this.vVelocity.y;
if (this.vPos.y > p.height + this.size) {
this.vPos.y = -this.size;
}
}
display() {
p.fill(this.hue, 30, 100, this.alpha);
p.push();
p.translate(this.vPos.x, this.vPos.y);
p.rotate(p.radians(this.vAngle.x));
p.beginShape();
for (let angle = 0; angle < 360 / 4; angle++) {
let A = (4 / p.PI) * p.radians(angle);
let mod = p.floor(A) % 2;
let r0 = p.pow(-1, mod) * (A - p.floor(A)) + mod;
let h = r0 < 0.8 ? 0 : 0.8 - r0;
let r = r0 + 2 * h;
let x = this.size * r * p.cos(p.radians(angle));
let y = this.yScale * this.size * r * p.sin(p.radians(angle));
p.vertex(x, y);
}
p.endShape(p.CLOSE);
p.pop();
}
}
p.setup = () => {
p.createCanvas(p.windowWidth, p.windowHeight).parent('sakura')
p.frameRate(30);
p.colorMode(p.HSB);
p.noStroke();
for (let i = 0; i < NUM; i++) {
petals.push(new Sakura());
}
};
p.draw = () => {
p.clear();
petals.forEach((petal) => {
petal.display();
petal.move();
});
};
p.windowResized = () => {
p.resizeCanvas(p.windowWidth, p.windowHeight);
petals = [];
for (let i = 0; i < NUM; i++) {
petals.push(new Sakura());
}
};
};
if (!document.body.classList.contains('hidden-class')) {
new p5(sakura);
}
</script>
<style>
body{
height:200vh;
}
body.hidden-class #sakura {
display: none;
}
#sakura {
position: fixed;
top: 0;
left: 0;
height: 100vh;
width: 100vw;
z-index:1000;
pointer-events: none;
}
h1{text-align:center;}
</style>