2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

p5.js preloadの仕組みを調べた

2
Posted at

はじめに

p5.js で 画像を読み込んだりするための処理 preload() は、すべての非同期読み込み処理が完了するまで setup() が開始されません。

画像を読み込んだりする処理は非同期のはずですが、どのようにしてすべての非同期preload処理が終わったことを認識するのか?どうやってsetup()を開始しているのか? 不思議だったので調べてみました。

次に、独自のpreload処理を書くにはどうしたらよいのかを試行錯誤して確認しましたので、その方法を記録します。

理解するには p5.js のコードを読み解く必要がありますが、読み解くためには「p5.js は何をしようとしているのか」を知っておいたほうがよいと思いますので、ここで概説をします(詳細は書ききれませんのですいません)

概説といいながらも、まとめるのが下手なので長めの記事になってしまいました(先に言い訳をしておきます)

p5.js の preloadの仕組みを理解すると何に役立つのか?

この方法は、p5.play の Sprite継承クラスが書かれた JSファイルを 動的にロードするための 読み込み処理を独自に作るときに役立ちます(役立つはずです)。

p5.js Sketch のかたち

次のコードのように p.preload() / p.setup() / p.draw() を持ちます。

p.preload() の中にかく非同期ロード処理は、setup() のなかで使うはずの様々なものをロードしています。

この読み込み処理が未完了の間は p.setup() が動き出さないように、p5.js で制御しているみたいです。

非同期処理なのに、どうしてそのようなことができるのか、疑問ですね。

というわけで調べてみました。

Sketch.jsの例

window.onload = () => {
  new p5( sketch );
};

const sketch = function {
    p.preload = function {
        // 次のような非同期処理を書くところ
        // p.loadImage('path');
        // p.loadXML('path');
        // p.loadFont('path')

        // ここに書かれた読み込み処理がすべて読み込み終了するまでは
        // setup() は動き出さない!
    }
    p.setup = function() {
        new p.Canvas(100,100); // キャンバスを作る
    }
    p.draw = function() {
        p.background(0); // 背景(黒=0) を書く
    }
};

p5.js preload非同期処理と setup()

配列 p5.prototype._preloadMethods

p5.js 内では下記の連想配列が用意されています。

p5.prototype._preloadMethods = {
    loadJSON: p5.prototype,
    loadImage: p5.prototype,
    loadStrings: p5.prototype,
    loadXML: p5.prototype,
    loadBytes: p5.prototype,
    loadTable: p5.prototype,
    loadFont: p5.prototype,
    loadModel: p5.prototype,
    loadShader: p5.prototype
};

キーは メソッド名、値は そのメソッドが存在する場所です。

p5 のインスタンスを作るタイミングで

p5 のインスタンスを作るタイミングにて次の処理が実行されます。

    var _methods2 = _this._preloadMethods;
    for (var method in _methods2) {
        // default to p5 if no object defined
        _methods2[method] = _methods2[method] || p5;
        var obj = _methods2[method];
        //it's p5, check if it's global or instance
        if (obj === p5.prototype || obj === p5) {
            if (_this._isGlobal) {
                 window[method] = _this._wrapPreload(_this, method);
            }
            obj = _this;
        }
        _this._registeredPreloadMethods[method] = obj[method];
        obj[method] = _this._wrapPreload(obj, method);
    }
    context.preload();
    _this._runIfPreloadsAreDone();

いろいろ書いてありますが、loadImageを例にとると、

  • loadImage メソッド を _wrapPreload()メソッドでラップする
  • ラップしたメソッドを 元の loadImage へ上書きする。
  • これで loadImageを呼び出すとラップされたメソッドが実行される。

_wrapPreload()でラップするとどうなるの?

    this._wrapPreload = function (obj, fnName) {
        var _this2 = this;
        return function () {
            //increment counter
            _this2._incrementPreload();
            //call original function
            for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
                args[_key] = arguments[_key];
            }
            return _this2._registeredPreloadMethods[fnName].apply(obj, args);
        };
    };

要するに オリジナルのメソッドを実行する前に、必ず _incrementPreload() を実行するよ!というわけです。

_incrementPreload() は何をするの?

    this._incrementPreload = function () {
        var context = this._isGlobal ? window : this;
        // Do nothing if we tried to increment preloads outside of `preload`
        if (context._preloadDone) return;
        context._setProperty('_preloadCount', context._preloadCount + 1);
    };

_preloadCount をインクリメントしています。
なお、preload()処理が終わっているとき(_preloadDone == true )は、何もしないです。

_preloadCount とは?

コードを読み解くと次の意味をもっているようです。

_preloadCount: 『非同期実行中の回数』

非同期読み込み処理は何をしているの?

loadImage を例にとります。

 _main.default.prototype.loadImage = function (path, successCallback, failureCallback) {
    _main.default._validateParameters('loadImage', arguments);
    var pImg = new _main.default.Image(1, 1, this);
          var self = this;
          var req = new Request(path, {
            method: 'GET',
            mode: 'cors'
          });
          fetch(path, req).then(function (response) {
            // GIF section
            var contentType = response.headers.get('content-type');
            if (contentType === null) {
                // 途中省略
            }
            if (contentType && contentType.includes('image/gif')) {
              response.arrayBuffer().then(function (arrayBuffer) {
                if (arrayBuffer) {
                  var byteArray = new Uint8Array(arrayBuffer);
                  _createGif(byteArray, pImg, successCallback, failureCallback, function (pImg) {
                    self._decrementPreload(); // ===> 重要!!
                  }.bind(self));
                }
              }, function (e) {
                if (typeof failureCallback === 'function') {
                  failureCallback(e);
                  self._decrementPreload(); // ===> 重要!!
                } else {
                  console.error(e);
                }
              });
            } else {
              // 以降 途中省略
            }
          }).catch(function (e) {
              // 以降 途中省略
          });
          return pImg;
        };


『 ===> 重要!!』のところは、非同期処理が終わるところです。
_decrementPreload() が書かれていることがわかると思います。
p5.js の preload() にて実行する 非同期処理は処理が終わった時点で_decrementPreload() を呼び出すことがルールになっているようです。

_decrementPreload() とは?

いよいよp5.js preload の キモに近づいてまいりました。

    this._decrementPreload = function () {
        var context = this._isGlobal ? window : this;
        if (!context._preloadDone && typeof context.preload === 'function') {
            context._setProperty('_preloadCount', context._preloadCount - 1);
            context._runIfPreloadsAreDone(); // ==> ここ重要!
        }
    };

解読すると、

  • preloadが終わっていない( _preloadDone == false ) とき『非同期実行中の回数』(_preloadCount) をデクリメントする
  • デクリメント後に _runIfPreloadsAreDone() を呼び出す

_runIfPreloadsAreDone() とは?

ここが探していたところです。p5.js preload の キモです。

    this._runIfPreloadsAreDone = function () {
        var context = this._isGlobal ? window : this;

        // 『非同期実行中の回数』がゼロのとき = 他に非同期処理が動いていないとき
        if (context._preloadCount === 0) {
            // 重要でないところを一部割愛しています。
            
            // setup()が始まっていないとき
            if (!this._setupDone) { 
                this._lastTargetFrameTime = window.performance.now();
                this._lastRealFrameTime = window.performance.now();
                context._setup();
                if (!this._recording) {
                    context._draw();
                }
            }
        }
    };

『非同期実行中の回数』をインクリメントして、非同期処理が終わったらデクリメントして、『非同期実行中の回数』=ゼロであれば他に非同期処理は動いていないとみなす、という感じです。

『非同期実行中の回数』=ゼロのときには、setup() と draw() が動き出します。

    context._setup();
    if (!this._recording) {
        context._draw();
    }

補足:draw() は、_draw() のなかで次の処理が書かれていて、繰り返し _draw が呼び出されることになっています。

window.requestAnimationFrame(_this._draw);

つまりどうゆうこと??

 p.preload = function(){
    picoImage1 = p.loadImage('./assets/Pico walk1.svg'); // (A)
    picoImage2 = p.loadImage('./assets/Pico walk2.svg'); // (B)
    fishImageA = p.loadImage('./assets/fish-a.svg');     // (C)
    fishImageB = p.loadImage('./assets/fish-b.svg');     // (D)
  };

preload() の中に複数の非同期読み込み処理を書くとします(上の例では loadImage() )

続けざまに 書かれているだけ呼び出されますが、非同期処理なのでどの順に完了するかはわかりません(ロードする対象のサイズとかターゲットがあるサーバとの通信時間とかが影響しそう)。

非同期(A),(B),(C),(D) の処理時間の様子を図で表現してみます。

(A) --------------> 終わり --> (A)の非同期処理が setup/draw を動かす
(B) ----> 終わり ( 終わるだけ )
(C) ------> 終わり ( 終わるだけ )
(D) ----> 終わり ( 終わるだけ )

最後に終わった非同期ロード処理(上の例では(A) )のなかで、setup()を実行、draw()の実行を開始、というわけです。preload() の中にかく非同期ロード処理は、次の処理を制御するという『重要な役割』をもつ(もたねばならない)というわけです。

p5.js における registerPreloadMethod()

p5 では次の非同期ロード処理が用意されています。

    loadJSON
    loadImage
    loadStrings
    loadXML
    loadBytes
    loadTable
    loadFont
    loadModel
    loadShader

独自の非同期処理を preload() の中に書くことはできるでしょうか?

  • 独自の非同期処理は終わるまで setup の実行を待機させたい

もちろんできます。registerPreloadMethod() を使って preloadメソッドを登録することになります。

registerPreloadMethod() を呼び出す場所

p5のinit処理で実行するのがよさそうです。

下記の例では dynamicLoadScript という名前で preloadメソッドを登録しています。
第二引数は、preloadメソッドが存在する場所です。p5.prototype にメソッドを作っています。

p5.prototype.registerMethod('init', function() {
    
    p5.prototype.registerPreloadMethod('dynamicLoadScript', p5.prototype);

    p5.prototype.dynamicLoadScript = function( _jsPath ) {
        const _promise = _dynamicLoadScript(_jsPath);
        _promise.then(()=>{
            p._decrementPreload(); // これを忘れると setup() が動かない。
        }
    }
}

const _dynamicLoadScript = function( _jsPath ) {
    return new Promise( (resolve,reject)=>{
        // ロードが終わったら resolveしてあげるような非同期処理を書く。
    });
};

雰囲気をお伝えしていますので _dynamicLoadScript の詳細は割愛します。

使い方

 p.preload = function(){
     p.dynamicLoadScript('.js/aaaaaa.js');
  };

他の標準の非同期ロードと同じように preload() の中で使えます。よかったね。

非同期ロード処理を登録して、preload() で使うサンプル

4個の JSファイルを動的に読み込み、使うサンプルです。
うち、2個は p5.play の Spriteクラスを継承するクラスを定義しています。

サンプルの実例

p5EditorのSketchをみてね

DynamicLoadScriptSample1

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.8.0/p5.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.8.0/addons/p5.sound.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/planck@latest/dist/planck.min.js"></script>
    <script src="https://p5play.org/v3/p5play.js"></script>
    <link rel="stylesheet" type="text/css" href="style.css">

  </head>
  <body>
    <main>
    </main>
    <script src="js/p5PlaySpriteFukidashi.js"></script>
    <script src="js/dynamicLoadScript.js"></script>
    <script src="sketch.js"></script>
  </body>
</html>

※ p5.play Spriteクラスを継承する クラス定義が書かれたJSファイルは scriptタグで宣言されていません。

dynamicLoadScript.js

ここで、JSファイルを動的にロードする非同期処理が書かれていて、それをpreloadメソッドとして登録しています。

dynamicLoadScriptの中身は、独自非同期ロード処理の紹介として掲載しています。
Javascriptを動的ロードする方法の解説を目的とはしていませんので、ご承知おきください。

p5.prototype.registerMethod('init', function() {
  
  // p5インスタンスを退避する
  const p = this;
  
  // p5インスタンスをwindowへ突っ込むことで
  // クラス定義をしているJSファイル内で p を直書きできる。
  window.p = p; 
  
  const _msTime = Date.now(); // Cache回避のために現在ミリ秒を取得する
  // preloadメソッドの登録
  p5.prototype.registerPreloadMethod('dynamicLoadScript', p5.prototype);

  // 非同期ロード処理を定義する
  p5.prototype.dynamicLoadScript = function(_file_url){
    const _promise = _dynamicLoadScript(_file_url, _msTime);
    _promise.then(()=>{
      p._decrementPreload();
    });
  }
});
/*
 * _dynamicLoadScript
 * Javascriptを読み込む処理をPromiseとして返す。
*/
const _dynamicLoadScript = function(_file_url, _msTime, _async = true, _type = "text/javascript") {
  
  const _url = `${_file_url}?ts=${_msTime}`;// Cache回避対策
  return new Promise((resolve,reject)=>{
    try {
      const scriptEle = document.createElement("script");
      scriptEle.type = _type;
      scriptEle.async = _async;
      scriptEle.src = _url;

      scriptEle.addEventListener("load", (ev) => {
        resolve({ status: true });
      });

      scriptEle.addEventListener("error", (ev) => {
        reject({
          status: false,
          message: `Failed to load the script ${_url}`
        });
      });
      document.body.appendChild(scriptEle);
      
    } catch (error) {
      reject(error);
    }
  });
}

↑ 非同期にJSファイルをロードする処理です。非同期なので、複数JSファイルをロードするときはロード順は保証されません(どれが先に終わるかは保証されません)。

直列に複数JSファイルをロードする方法

ここの趣旨から逸脱しますので、別のQiita記事にてご紹介しますね。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?