TurbowarpでP5JSを動かす

# 目標：TurbowarpでP5JSを動かす

P5JS の描画の仕組みを可能な限り邪魔をせず、Turbowarpの上でP5JSを動かすことを目標にします。
とりあえず、TurbowarpのStageへ描画を行うようにしたいと思います。
- 線を引く
- frameCountにより角度を計算し線を回転させる


## 描画の間隔
Turbowarp(=Scratch3.x)はFPS(=30)の間隔で描画を繰り返しています。
一方 P5JS内でも FPS間隔で描画を繰り返す処理があります( 通常FPS=60みたいです）。
この２つが干渉しあわずに いい感じでP5JS描画を行わせてみたいと思います。

## P5JSにおけるコードの基本形

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294946/a797b41c-dd25-46da-a9e8-f494be9f66b0.png)

`s` は `sketch`の頭文字を意味するようです。`p5`のインスタンスは一般的に`p` と表記するようです。

sketchのなかで、setupメソッド、drawメソッドを定義し、p5のインスタンスを作成します。これで、P5JSは drawメソッドを FPS間隔ごとに実行を繰り返します。

## こうしてみたい

Turbowarp 側の繰り返しブロックの中のDRAWブロックより P5のdrawを呼び出したい、
TurbowarpのFPS間隔で P5のdrawを実行したい

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294946/8dc41d18-f073-dd09-d34e-c2f4e078a110.png)

# サンプル動画

何をしようとしているのかを知ってもらうため、動作する様子を先に見てもらおうと思います。
出来上がった拡張機能コード(Extension.js)をTurbowarpへ取り込んで動かしてみました。
本記事内、STEP03で説明した版を動かしています。

<iframe width="1280" height="720" src="https://www.youtube.com/embed/50wzGUsR2Js" title="" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

------------------
# 前提知識

Turbowarpのカスタム拡張機能
私の記事ごときで恐縮ですが、次の記事が参考になるといいです。
- [『カスタム拡張機能』を使おう【１】](https://qiita.com/harhid/items/d58557453d2b39f5071f)：基本の説明と外部JSファイルを動的Importの方法
- [『カスタム拡張機能』を使おう【２】](https://qiita.com/harhid/private/31a3599de2f1cbec8024)：拡張機能定義の主要項目を補足したもの
- [『カスタム拡張機能』を使おう【３】](https://qiita.com/harhid/private/b07e8456c23d4c7a8de3)：動的Importするファイルを変更できるようにする
- [『カスタム拡張機能』を使おう【４】](https://qiita.com/harhid/private/65fbc3cc05bcff6336de)：外部JSファイルにてScratchブロック処理を再現
- [『カスタム拡張機能』を使おう【５】](https://qiita.com/harhid/items/c1ccb5cc4af66ba0cac1)：外部JSファイルにてスピーチ処理を再現

---------------
# P5JSのdraw実行の仕組みを読み解く

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294946/9bb0dced-d59f-e32e-d09d-280aa99b518a.png)

#### ① 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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294946/b609fec7-798d-de1e-fb35-ad6533ec42df.png)

## P5JS DRAW

ブロックを実行すると、P5.`_draw` を呼び出すことにします。そうすると、P5JSの描画処理どおりに、P5.`_draw` ⇒ P5.`redraw` ⇒ P5.`draw` と実行されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294946/e3de56e6-6b25-dc36-aeca-b85038a0abce.png)

## 利点

P5JSの描画処理どおりにP5.drawを実行する利点は次のとおりです。

### P5JS描画ルールを順守
知らないうちにP5JSを使ううえで必要なことをすっ飛ばしている！ということがなさそう。安心です。
P5JSで作った作品のコードを参考にするとき使えないテクニックがなくなりそうです。

### P5.frameCount 
p5.drawの実行回数分、frameCountがカウントアップされます。何回目の描画なのかをしることで、描画に動きを与えることができます。


# STEP01(基本形）

## Extension.js
[コードGITHUB](https://github.com/amami-harhid/turbowarpExtensions/blob/main/_06_01_extension/Extension.js)

#### p5jsImport

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294946/06d79906-b10d-54dd-9a4d-514221caa7fe.png)

P5JS をインポートします。

```javascript:Extension.js
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
         },
       },
    },
  }
]
```
```javascript
// P5JS CDN URL
const P5JSLIB 
  = "https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.4/p5.js";
```
```javascript
 /**
  * P5JSをインポートする
  * @param {*} args 
  * @param {*} util 
  */
 async p5jsImport( args, util ){
   try{
     // ここで P5JS CDN LIB を読み込む(キャッシュＯＫ)
     await import(P5JSLIB);

   }catch(e){
     const mesagge = 'P5JSの読み込みに失敗したみたいです'
     console.error( mesagge, e );
     alert(mesagge);
   }
 }
```
#### p5JsStart

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294946/674ebb48-0337-642c-9110-a29abda33a60.png)

P5JSのSketchをインポートし、P5JSをインスタンスモードで開始します。SketchファイルはExtension.jsとは別のJSファイルです。

```javascript:Extension.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
          },
       },
  },
]
```

```javascript
// テスト用JSファイルの場所(HOST+DIRCTORY)
const TEST_URL 
        = 'http://127.0.0.1:5500/_06_01_extension';
```
```javascript
 /**
  * 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
[コードGITHUB](https://github.com/amami-harhid/turbowarpExtensions/blob/main/_06_01_extension/sketch.js)

```javascript: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
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294946/318bbc63-eb01-f0f4-7d79-a4186caf2e5c.png)

```javascript:Extension.js
blocks: [
  {
    opcode: "p5JsDraw",
    blockType: Scratch.BlockType.COMMAND,
    text: "[IMG_GEAR]P5JS描画をする",
    arguments: {
       IMG_GEAR: {
          type: Scratch.ArgumentType.IMAGE, //タイプ
          dataURI: GEAR_IMAGE_SVG_URI,      //歯車画像のURI
       },
    },    
  },
]
```
```javascript
 /**
  * Scratch3.x(=Turbowarp)のブロックから呼び出されるdraw処理
  * @param {*} args 
  * @param {*} util 
  */
 async p5JsDraw( args, util ) {
    this.p._draw();
 }
```

# STEP02(CreateCanvas）

STEP01 では、p5のcanvasを作成していないため、描画結果を見ることができません。p5のsetupメソッドのなかで、次の処理を実行する必要があります。

```javascript
  // 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のなかで書くことにします。

[コードGITHUB](https://github.com/amami-harhid/turbowarpExtensions/blob/main/_06_02_extension/Extension.js)

```javascript:Extension.js
  /**
   * P5JSをインポートする
   * @param {*} args 
   * @param {*} util 
  */
  async p5jsImport( args, util ){
    try{
      // ここで P5JS CDN LIB を読み込む(キャッシュＯＫ)
      await import(P5JSLIB);
      
      // ★★★★★★★★★★★★★★★
      // ★★★★ ここから追記 ★★★★
      // ★★★★★★★★★★★★★★★
      
      // 【P5JS フックの登録】(beforeSetup)
      // p5.setup実行直前に呼び出すフックを登録する処理。
      // StageのCanvasをSketchの中で直接には参照できないので
      // utilが参照できる箇所で定義した。
      // -- 注意：フック(init)を使うのはうまくいかない
      // -- Sketch内のp.setupの定義のタイミングより前に
      // -- initのフックの処理が実行される。これではp.setupの
      // -- 置換を行えないため、タイミングとしては不適切。
      p5.prototype.registerMethod('beforeSetup', function(){
        // このフックは１回だけ実行されることになっている
        // フック実行時、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 --
上記と同じく利用に適していません


```javascript:sketch.js
/**
 * 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`を使うことにします。

```javascript
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)」の中で実装しましょう。

[コードGITHUB](https://github.com/amami-harhid/turbowarpExtensions/blob/main/_06_03_extension/Extension.js)


```javascript:Extension.js
  /**
   * P5JSをインポートする
   * @param {*} args 
   * @param {*} util 
  */
  async p5jsImport( args, util ){
    try{
      // ここで P5JS CDN LIB を読み込む(キャッシュＯＫ)
      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);
    }
  }
```