目標:TurbowarpでP5JSを動かす
P5JS の描画の仕組みを可能な限り邪魔をせず、Turbowarpの上でP5JSを動かすことを目標にします。
とりあえず、TurbowarpのStageへ描画を行うようにしたいと思います。
- 線を引く
- frameCountにより角度を計算し線を回転させる
描画の間隔
Turbowarp(=Scratch3.x)はFPS(=30)の間隔で描画を繰り返しています。
一方 P5JS内でも FPS間隔で描画を繰り返す処理があります( 通常FPS=60みたいです)。
この2つが干渉しあわずに いい感じでP5JS描画を行わせてみたいと思います。
P5JSにおけるコードの基本形
s
は sketch
の頭文字を意味するようです。p5
のインスタンスは一般的にp
と表記するようです。
sketchのなかで、setupメソッド、drawメソッドを定義し、p5のインスタンスを作成します。これで、P5JSは drawメソッドを FPS間隔ごとに実行を繰り返します。
こうしてみたい
Turbowarp 側の繰り返しブロックの中のDRAWブロックより P5のdrawを呼び出したい、
TurbowarpのFPS間隔で P5のdrawを実行したい
サンプル動画
何をしようとしているのかを知ってもらうため、動作する様子を先に見てもらおうと思います。
出来上がった拡張機能コード(Extension.js)をTurbowarpへ取り込んで動かしてみました。
本記事内、STEP03で説明した版を動かしています。
前提知識
Turbowarpのカスタム拡張機能
私の記事ごときで恐縮ですが、次の記事が参考になるといいです。
- 『カスタム拡張機能』を使おう【1】:基本の説明と外部JSファイルを動的Importの方法
- 『カスタム拡張機能』を使おう【2】:拡張機能定義の主要項目を補足したもの
- 『カスタム拡張機能』を使おう【3】:動的Importするファイルを変更できるようにする
- 『カスタム拡張機能』を使おう【4】:外部JSファイルにてScratchブロック処理を再現
- 『カスタム拡張機能』を使おう【5】:外部JSファイルにてスピーチ処理を再現
P5JSのdraw実行の仕組みを読み解く
① P5._draw の実行
P5JSが動き出し、P5初期処理が終わると 内部メソッドである P5._draw
が呼び出されます。
② P5.redraw の実行
P5._draw の中から P5.redraw が呼び出されます。
③ P5._renderer.update の実行
_renderer
.update
を呼び出すことで、Canvas が初期化されます。
④ P5.draw
の実行
定義しておいた P5.draw
が呼び出されます。
つまり、P5._draw
が呼び出されると P5._draw
⇒ P5.redraw
⇒ P5.draw
と実行されることがわかります。
⑤ 自動ループ
FPSの時間経過後に 再度 P5._draw
を実行します。これにより 繰り返しの描画処理が実現できています。
P5JSではデフォルトで自動ループすることになっています。
自動ループを抑止したい場合は、初期処理で P5.noLoop
を実行しておけばOKです。
Turbowarpブロックから P5.drawを動かす
P5JSのdrawの仕組みを可能な限り尊重しつつ、TurbowarpブロックからP5.draw
を実行する仕組みを考えてみます。
P5JS IMPORT
P5JS DRAW
ブロックを実行すると、P5._draw
を呼び出すことにします。そうすると、P5JSの描画処理どおりに、P5._draw
⇒ P5.redraw
⇒ P5.draw
と実行されます。
利点
P5JSの描画処理どおりにP5.drawを実行する利点は次のとおりです。
P5JS描画ルールを順守
知らないうちにP5JSを使ううえで必要なことをすっ飛ばしている!ということがなさそう。安心です。
P5JSで作った作品のコードを参考にするとき使えないテクニックがなくなりそうです。
P5.frameCount
p5.drawの実行回数分、frameCountがカウントアップされます。何回目の描画なのかをしることで、描画に動きを与えることができます。
STEP01(基本形)
Extension.js
p5jsImport
P5JS をインポートします。
blocks: [
{
opcode: "p5jsImport",
blockType: Scratch.BlockType.COMMAND,
text: "[IMG_GEAR]P5JS IMPORT",
arguments: {
IMG_GEAR: {
type: Scratch.ArgumentType.IMAGE, //タイプ
dataURI: GEAR_IMAGE_SVG_URI, //歯車画像のURI
},
},
},
}
]
// P5JS CDN URL
const P5JSLIB
= "https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.js";
/**
* P5JSをインポートする
* @param {*} args
* @param {*} util
*/
async p5jsImport( args, util ){
try{
// ここで P5JS CDN LIB を読み込む(キャッシュOK)
await import(P5JSLIB);
}catch(e){
const mesagge = 'P5JSの読み込みに失敗したみたいです'
console.error( mesagge, e );
alert(mesagge);
}
}
p5JsStart
P5JSのSketchをインポートし、P5JSをインスタンスモードで開始します。SketchファイルはExtension.jsとは別のJSファイルです。
blocks: [
{
opcode: 'p5JsStart',
blockType: Scratch.BlockType.COMMAND,
text: "[IMG_GEAR]p5Jsを開始する",
arguments: {
IMG_GEAR: {
type: Scratch.ArgumentType.IMAGE, //タイプ
dataURI: GEAR_IMAGE_SVG_URI, //歯車画像のURI
},
},
},
]
// テスト用JSファイルの場所(HOST+DIRCTORY)
const TEST_URL
= 'http://127.0.0.1:5500/_06_01_extension';
/**
* p5JS setup を開始する
* @param {*} args
* @param {*} util
*/
async p5JsStart( args, util ){
try{
// Sketchを読み込む(キャッシュからの読み込みをしない)
const _t = new Date().getTime();
const sketchUrl = `${TEST_URL}/sketch.js?_t=${_t}`;
const {sketch} = await import( sketchUrl );
const p = new p5(sketch);
this.p = p;
}catch(e){
const mesagge = 'Sketchの読み込みに失敗した、'
+'もしくはP5JSインスタンスモード開始に失敗した';
console.error( mesagge, e );
alert(mesagge);
}
}
sketch.js
/**
* P5JS Sketch
* @param {*} p
*/
const sketch = (p) => {
p.setup = () => {
alert('sketch setup'); // DEBUG用にいれたalert文
}
p.draw = () => {
// 背景の色
p.background( 150, 150, 150 );
}
}
export {sketch};
p5JsDraw
blocks: [
{
opcode: "p5JsDraw",
blockType: Scratch.BlockType.COMMAND,
text: "[IMG_GEAR]P5JS描画をする",
arguments: {
IMG_GEAR: {
type: Scratch.ArgumentType.IMAGE, //タイプ
dataURI: GEAR_IMAGE_SVG_URI, //歯車画像のURI
},
},
},
]
/**
* Scratch3.x(=Turbowarp)のブロックから呼び出されるdraw処理
* @param {*} args
* @param {*} util
*/
async p5JsDraw( args, util ) {
this.p._draw();
}
STEP02(CreateCanvas)
STEP01 では、p5のcanvasを作成していないため、描画結果を見ることができません。p5のsetupメソッドのなかで、次の処理を実行する必要があります。
// TurbowarpのStageのCanvas
const canvas = util.target.renderer.gl.canvas;
const w = canvas.clientWidth;
const h = canvas.clientHeight;
// StageのCanvasを利用して p5のcanvasにする
p.createCanvas(w, h, p.WEBGL, canvas);
utilは、Turbowarpブロックから渡される引数です。
ここで問題があります。sketchのsetupメソッドにて util
を 直接に参照することができないことです。
p5のsetupメソッドを sketchのなかではなく、util
を参照できる場所で記述する必要があります。
そこで setupメソッドの中身の「一部」を Extension.jsのなかで書くことにします。
/**
* P5JSをインポートする
* @param {*} args
* @param {*} util
*/
async p5jsImport( args, util ){
try{
// ここで P5JS CDN LIB を読み込む(キャッシュOK)
await import(P5JSLIB);
// ★★★★★★★★★★★★★★★
// ★★★★ ここから追記 ★★★★
// ★★★★★★★★★★★★★★★
// 【P5JS フックの登録】(beforeSetup)
// p5.setup実行直前に呼び出すフックを登録する処理。
// StageのCanvasをSketchの中で直接には参照できないので
// utilが参照できる箇所で定義した。
// -- 注意:フック(init)を使うのはうまくいかない
// -- Sketch内のp.setupの定義のタイミングより前に
// -- initのフックの処理が実行される。これではp.setupの
// -- 置換を行えないため、タイミングとしては不適切。
p5.prototype.registerMethod('beforeSetup', function(){
// このフックは1回だけ実行されることになっている
// フック実行時、thisは p5インスタンスである
const p = this;
// Sketchにsetupが登録されているときSketchのsetupを上書きする
if(p.setup){
// 【StageのCanvasをP5jsのCanvasとして利用】
const _reuseCanvas = () => {
// StageのCanvasを取得する
const canvas = util.target.renderer.gl.canvas;
const w = canvas.clientWidth;
const h = canvas.clientHeight;
// StageのCanvasをp5jsのCanvasとして使う
p.createCanvas(w, h, p.WEBGL, canvas);
}
// 【Sketchのsetupを置換】
// createCanvasは フックの中で直接に実行してはいけない
// setup の中で createCanvasを実行するように
// sketchのsetupを置き換える
const _sketchSetup = p.setup;
const _wrapper = () => {
// drawの繰返しを抑止する
p.noLoop();
// StageのCanvasをP5jsのCanvasとして利用する
_reuseCanvas();
// 元のsetupを実行する
_sketchSetup();
}
p.setup = _wrapper;
}
});
// ★★★★★★★★★★★★★★★
// ★★★★ ここまで追記 ★★★★
// ★★★★★★★★★★★★★★★
}catch(e){
const mesagge = 'P5JSの読み込みに失敗したみたいです'
console.error( mesagge, e );
alert(mesagge);
}
}
p5jsImportメソッドのなかで、p.setup を置換し、StageのCanvasを利用してcreateCanvasを実行しています。
P5JSLIBをインポートした直後に、P5へフック(beforeSetup
)を登録することで実現しています。P5がsetupを実行する直前のタイミングで、setupのなかで createCanvas を実行するように、setupを置換しています。
setupの前に実行される P5のフックには、他、init, beforePreload, afterPreload がありますが、いずれも利用には適していません。
-- init --
initが実行される時点では sketchにある p.setup の反映がまだ行われておらず、p.setup は存在していません。p.setupを入れ替えることができないため、利用に適していません
-- beforePreload --
sketchに p.preloadの定義がないとき、このフックは実行されず、利用に適していません
-- afterPreload --
上記と同じく利用に適していません
/**
* P5JS Sketch
* @param {*} p
*/
const sketch = (p) => {
p.setup = () => {
alert('sketch setup'); //DEBUG用のalert
}
p.draw = () => {
const w = p.canvas.clientWidth;
const halfWidth = w / 2;
const length = halfWidth * 0.5;
// 回転角度を計算
const f = p.frameCount; // p.redrawの中でカウントアップ
const degree = f * 2; // <-- 大きければ早く回転する
const radians = degree * Math.PI / 180;
// 回転した先の点
const dx = length * Math.cos(radians);
const dy = length * Math.sin(radians);
// 背景の色
p.background( 150, 150, 150 );
// 線の色
p.stroke( 255, 255, 255 );
// 線の太さ
p.strokeWeight(1);
// 線を引く
p.line( -dx, -dy, dx, dy );
p.line( -length, 0, length, 0 );
}
}
export {sketch};
p.setup の中身には createCanvasはありません。実際のcreateCanvasは、フック(beforeSetup)にて実装されています。
p.draw には 線を引く実装が書かれています。
STEP03(ResizeCanvas)
STEP02にて、TurbowarpのStageにP5JSによる描画ができるようになりました。
しかし、Turbowarp操作でStageの大きさを変えるとStageへ描画をしなくなります。Stage(小) ⇔ Stage(大) と切り替えた後は、P5JSを再起動しないと描画成功しません。P5JSのCanvasの大きさがStageの大きさになっているのですが、Stageの大きさの変化を P5JSのCanvasへ自動反映する仕組みがないためです。
STEP03では、Stageのサイズを監視する仕組みを作り、変化があったときはP5JSのCanvasをリサイズするようにします。監視はMutationObserver
を使うことにします。
const canvas = util.target.renderer.gl.canvas;
const observer = new MutationObserver(() => {
_resizeCanvas(); // Canvasのリサイズ
};
// Scratch3.xのキャンバスサイズ変更は、style属性の値が
// 変化しているため、style属性の変化を監視する。
observer.observe(canvas, {
attriblutes: true,
attributeFilter: ["style"],
});
上記のコードを、STEP02で紹介した「フック(beforeSetup)」の中で実装しましょう。
/**
* P5JSをインポートする
* @param {*} args
* @param {*} util
*/
async p5jsImport( args, util ){
try{
// ここで P5JS CDN LIB を読み込む(キャッシュOK)
await import(P5JSLIB);
// 【P5JS フックの登録】(beforeSetup)
p5.prototype.registerMethod('beforeSetup', function(){
const p = this;
if(p.setup){
// 【StageのCanvasをP5jsのCanvasとして利用】
const _reuseCanvas = () => {
// StageのCanvasを取得する
const canvas = util.target.renderer.gl.canvas;
const w = canvas.clientWidth;
const h = canvas.clientHeight;
// StageのCanvasをp5jsのCanvasとして使う
p.createCanvas(w, h, p.WEBGL, canvas);
}
// ★★★★★★★★★★★★★★★
// ★★★★ ここから追記 ★★★★
// ★★★★★★★★★★★★★★★
// 【Stageサイズ変化監視】
// Stageサイズの変化をMutationObserverにて監視し
// サイズ変更時はサイズ変更後のCanvasで再度使用宣言をする
const _resizeCanvas = _reuseCanvas;
const _stageSizeObserver =()=>{
const canvas = util.target.renderer.gl.canvas;
// Stageサイズ変化時に resize処理をする
const observer = new MutationObserver(() => {
_resizeCanvas();
});
// Scratch3.xのキャンバスサイズ変更は、style属性の値が
// 変化しているため、style属性の変化を監視する。
observer.observe(canvas, {
attriblutes: true,
attributeFilter: ["style"],
});
};
// ★★★★★★★★★★★★★★★
// ★★★★ ここまで追記 ★★★★
// ★★★★★★★★★★★★★★★
// 【Sketchのsetupを置換】
const _sketchSetup = p.setup;
const _wrapper = () => {
p.noLoop();
_reuseCanvas();
// ★★★★★★★★★★★★★★★
_stageSizeObserver(); //【追加】Stageサイズ変化を監視する
// ★★★★★★★★★★★★★★★
_sketchSetup();
}
p.setup = _wrapper;
}
});
}catch(e){
const mesagge = 'P5JSの読み込みに失敗したみたいです'
console.error( mesagge, e );
alert(mesagge);
}
}