0
0

TurbowarpでP5JSを動かす

Last updated at Posted at 2024-08-07

目標:TurbowarpでP5JSを動かす

P5JS の描画の仕組みを可能な限り邪魔をせず、Turbowarpの上でP5JSを動かすことを目標にします。
とりあえず、TurbowarpのStageへ描画を行うようにしたいと思います。

  • 線を引く
  • frameCountにより角度を計算し線を回転させる

描画の間隔

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

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

image.png

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

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

こうしてみたい

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

image.png

サンプル動画

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


前提知識

Turbowarpのカスタム拡張機能
私の記事ごときで恐縮ですが、次の記事が参考になるといいです。


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

image.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

P5JS DRAW

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

image.png

利点

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

P5JS描画ルールを順守

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

P5.frameCount

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

STEP01(基本形)

Extension.js

コードGITHUB

p5jsImport

image.png

P5JS をインポートします。

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
         },
       },
    },
  }
]
// 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

image.png

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

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
          },
       },
  },
]
// テスト用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

コードGITHUB

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

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
       },
    },    
  },
]
 /**
  * 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のなかで書くことにします。

コードGITHUB

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

上記と同じく利用に適していません

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を使うことにします。

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

Extension.js
  /**
   * 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);
    }
  }
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0