Browser one click behind when loading different p5 sketches on click

edited December 2016 in p5.js Library Questions

Hi everybody, I've been dealing with this problem for some time now and I can't figure out what I'm doing wrong.

I'm building a website with Jekyll. A lot of people will contribute on different javascript sketches that will be displayed on the home page: some of them will be written with P5.js, other with other libraries.

Since I didn't want to load all the libraries on page load I generated a JSON in which I specify the sketch to load and all the associated libraries:

[
   {
      "author":"Federico Pepe",
      "description":"javascript, Processing",
      "href":"http://localhost:4000/js/hero/federico.js",
      "includes":[
         "https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.4/p5.min.js",
         "https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.4/addons/p5.dom.min.js"
      ],
      "kind":"javascript",
      "title":"Lavoro di Federico Pepe"
   },
   {
      "author":"Alka Cappellazzo",
      "description":"Processing",
      "href":"http://localhost:4000/js/hero/alka.js",
      "includes":[
         "https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.4/p5.min.js",
         "https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.4/addons/p5.dom.min.js"
      ],
      "kind":"javascript",
      "title":"Lavoro di Alka"
   }
]

I then use a random function to select one of the script in that JSON and then, by using jQuery, I load all the scripts asynchronously and in order:

var loadedData;

$(document).ready(function() {
    // Load all the scripts in an array
    $.get("{{ site.url }}/homepage.json").done(function(data){
      loadedData = data;
      item = loadedData[Math.floor(Math.random() * data.length)];
      deferredLoading();
    });

    $("#jsRefresh").click(function() {
      deferredLoading();
    });
});

and then the function:

function deferredLoading() {

  $("canvas").remove();
  $("#loading").show();

  var dfd = $.Deferred();

  item = loadedData[Math.floor(Math.random() * loadedData.length)];

  for(i in item.includes) {
    dfd.done($.get(item.includes[i]).done());
  }

  $.when(dfd).done(
    $.get(item.href).done(function() {
      $("#loading").hide();
      $(".item-title").html(item.title + ' | ' + item.author);
      $(".item-description").html(item.description);

    })
  )

  dfd.resolve();
}

Now the problem is: when I click the link to fire the function deferredLoading() again, the canvas won't update. If I click the link again the canvas will update but with the script loaded previously.

I made some debugging and figured out that it's a P5.js issue because if I put a simple alert() in the JS I'm loading instead of a sketch, it's loaded properly when I click the link to refresh it.

Any help would be really appreciated

Answers

  • edited December 2016
    • Dunno much about jQuery, much less Jekyll. X_X
    • So those get(), done(), when(), Deferred(), etc. demand a deep study for me to understand.
    • Nonetheless, I've looked up jQuery's API. And found out I could make things a bit easier for me by turning off async loading via $.ajaxSetup({ async: false });. :ar!
    • Now a simple getScript() is enough to grab and run libraries in the correct order:
      http://api.jQuery.com/jQuery.getScript/
    • W/ that sorted out, my attention now is aimed at what could be those mysterious glitches you've reported above.
    • Given I don't have your Jekyll setup to run your code, most I can do is shoot in the dark here. 8-X
    • p5.js got some peculiarities like global & instance modes and how it checks the existence of setup() or draw() within global scope for auto-kickstart: :-?
      https://GitHub.com/processing/p5.js/wiki/Instantiation-Cases
    • Below the relevant excerpt direct from "p5.js" file:
      https://CDNjs.CloudFlare.com/ajax/libs/p5.js/0.5.5/p5.js
    var _globalInit = function() {
      if (!window.PHANTOMJS && !window.mocha) {
        // If there is a setup or draw function on the window
        // then instantiate p5 in "global" mode
        if(((window.setup && typeof window.setup === 'function') ||
           (window.draw && typeof window.draw === 'function')) &&
           !p5.instance) {
          new p5();
        }
      }
    };
    
    // TODO: ???
    if (document.readyState === 'complete') {
      _globalInit();
    } else {
      window.addEventListener('load', _globalInit , false);
    }
    
    • According to the init implementation above, we can nullified it by making PHANTOMJS or mocha to "exist" in the global scope.
    • Here's the hack for it: window.mocha || (window.mocha = 'truthy')
    • W/ that statement active, I can control when to kickstart the global mode by issuing:
      window.p5 && (window.setup || window.draw) && new p5;
    • Also I've added the option to 1st try to invoke remove(), having $("canvas").remove(); as fallback:
      http://p5js.org/reference/#/p5/remove
    • Be aware that the latter won't clear the p5 instance from keeping running and wasting RAM! :-SS
    • It's merely some visual workaround by getting rid of the canvas only. :|
    • W/o further ado, below's my shot-in-the-dark attempt. :bz
    • Recall I can't run it myself here. You're gonna need to test it by yourself: =;
    /**
     * Random p5.js Loader (v2.1.1)
     * by  FedPep   (2016-Dec-13)
     * mod GoToLoop (2016-Dec-19)
     *
     * https://forum.Processing.org/two/discussion/19725/
     * browser-one-click-behind-when-loading-different-p5-sketches-on-click#Item_2
     */
    
    $(document).ready(() => {
      "use strict";
    
      const isAsync = { async: true },
            jLoading = $('#loading'),
            jTitle = $('.item-title'),
            jDescript = $('.item-description'),
            JSON_URL = 'https://CodiceInutile.GitHub.io/website-dev/homepage.json';
    
      let json, item, idx;
    
      $.getJSON(JSON_URL, data => {
        if (!data.length)  return jTitle.html('ERROR! Empty JSONArray!!!');
    
        $.ajaxSetup({ cache: true });
        window.mocha || (window.mocha = 'Hack to stop global auto run for p5.js');
    
        json = data;
        pickAnotherItem(), loadScripts();
    
        $('#jsRefresh').click(() => loadScripts(pickAnotherItem()));
      })
       .fail(() => jTitle.html('ERROR! JSON file loading failed!!!'));
    
      function intRandom(n = 2) { return n * Math.random() | 0; }
    
      function pickAnotherItem() {
        if (json.length === 1)  return item = json[1];
    
        let rnd;
        while ((rnd = intRandom(json.length)) === idx);
    
        return item = json[idx = rnd];
      }
    
      function setAsync(bool = true) {
        isAsync.async = bool;
        $.ajaxSetup(isAsync);
      }
    
      function p5RemoveHack() {
        if (window.p5 && p5.instance) {
          p5.instance.remove();
          p5.instance = window.setup = window.draw = null;
        } else {
          const canv = document.querySelector('canvas');
          canv && canv.remove();
        }
      }
    
      function loadScripts() {
        jLoading.show();
        p5RemoveHack();
    
        setAsync(false);
        for (let lib of item.includes)  $.getScript(lib);
    
        setAsync(true);
        $.getScript(item.href, sketchLoaded);
      }
    
      function sketchLoaded() {
        jLoading.hide();
        jTitle.html(item.title + ' | ' + item.author);
        jDescript.html(item.description);
    
        window.p5 && (window.setup || window.draw) && new p5;
      }
    });
    
  • Thank you for your reply!

    Before your answered I edited the code a little and made it available on jsFiddle so you can fork, edit and run it without having to install jQuery or Jekyll.

    The glitch I'm currently having is that the canvas is not loaded on page load (but the script is!) and the refresh is still one click behind.

    Now I'm going through what you suggested above to see if I can manage to make it work properly even though, as I said, since I don't know how many script will be written for P5.js I'm considering other ways to display projects on the homepage.

  • edited March 2017 Answer ✓

    Finally I've made it. In version 2.x.x, I thought $.ajaxSetup({ async: false }); would be enough to make getScript() to load all scripts orderly, 1 by 1.

    Unfortunately it didn't make any apparent effect. ~X(
    Although file "p5.min.js" was set out to load 1st, "p5.dom.min.js" still finished 1st b/c it was much smaller. :(

    However, "p5.dom.min.js" depends on "p5.min.js" to exist 1st. Thus it crashed upfront! :-O

    Now the fix is to implement a recursive function + an array w/ all script paths to load at their correct order:

    scripts = [...item.includes, item.href];
    scripts.length && loadOrdered();
    
    function loadOrdered() {
      $.getScript(scripts.shift(), scripts.length && loadOrdered || sketchLoaded);
    }
    
    • When invoked, it removes array's head element via shift(), passing it as 1st argument for getScript().
    • For the 2nd callback argument, it checks out whether the array still got any length left.
    • If that's the case, it passes loadOrdered() itself as callback, effectively recurring the whole process once the script is successfully loaded.
    • Otherwise, it breaks the recursion by passing sketchLoaded() instead, which is the finalizing callback when all scripts are done. :>
    • Now everything is loaded and run at their expected order! :P

    You can check it out the complete program running online here: :-bd
    http://CodePen.io/GoSubRoutine/pen/ZBPjdP/left/?editors=101

    And below's the complete ordeal: :)>-

    "index.html":

    <script src=http://CDN.JSDelivr.net/jquery/latest/mainfile></script>
    <script src=loader.js></script>
    
    <div class="cover flexbox-container" id=myContainer>
      <div id=loading><h2>Loading</h2></div>
      <div class=title>
        <ul>
          <li class=item-title></li>
          <li class=item-description></li>
        </ul>
        <ul>
          <li><a id=jsRefresh href=#><b>Run Another Random Sketch</b></a></li>
        </ul>
      </div>
    </div>
    

    "loader.js":

    /**
     * Random p5.js Loader (v3.0.2)
     * by  FedPep   (2016-Dec-13)
     * mod GoToLoop (2016-Dec-21)
     *
     * https://forum.Processing.org/two/discussion/19725/
     * browser-one-click-behind-when-loading-different-p5-sketches-on-click#Item_4
     *
     * https://CodiceInutile.GitHub.io/website-dev/
     * http://jsFiddle.net/fedpep/uf3ve3nc/
     *
     * http://CodePen.io/GoSubRoutine/pen/ZBPjdP/left/?editors=101
     */
    
    "use strict";
    
    $(document).ready(() => {
      const $loading = $('#loading'),
            $title = $('.item-title'),
            $descript = $('.item-description'),
            JSON_URL = 'https://CodiceInutile.GitHub.io/website-dev/homepage.json',
            actions = () => (pickAnotherItem(), loadScripts()),
            intRandom = (n = 2) => n * Math.random() | 0;
    
      let json, item, idx, scripts;
    
      $.getJSON(JSON_URL, data => {
        if (!data.length)  return $title.html('ERROR! Empty JSONArray!!!');
    
        $.ajaxSetup({ cache: true });
        window.mocha || (window.mocha = 'Hack to stop global auto run for p5.js');
    
        json = data, actions();
        $('#jsRefresh').click(actions);
      })
       .fail(() => $title.html('ERROR! JSON file loading failed!!!'));
    
      function pickAnotherItem() {
        if (json.length === 1)  return item = json[0];
        for (var rnd; (rnd = intRandom(json.length)) === idx; );
        return item = json[idx = rnd];
      }
    
      function loadScripts() {
        $loading.show();
        p5RemoveHack();
    
        scripts = [...item.includes, item.href];
        scripts.length && loadOrdered();
      }
    
      function loadOrdered() {
        $.getScript(scripts.shift(), scripts.length && loadOrdered || sketchLoaded);
      }
    
      function sketchLoaded() {
        $loading.hide('slow');
        $title.html(item.title + ' | ' + item.author);
        $descript.html(item.description);
    
        window.p5 && (window.setup || window.draw) && new p5;
      }
    
      function p5RemoveHack() {
        if (window.p5 && p5.instance) {
          p5.instance.remove();
          p5.instance = window.setup = window.draw = null;
        } else {
          const canv = document.querySelector('canvas');
          canv && canv.remove();
        }
      }
    });
    
  • Thank you :)>-

    You're a wizard! ^:)^

  • As you've discovered an ajax request is something that finishes when it's ready :)

    ...and regardless of what else is happening in your code. As the jQuery docs also make clear, geScripts is just a wrapper around $.ajax - so does nothing to ensure multiple getScript calls complete in a specific order: the success callback simply fires when the associated request completes.

    You could resolve your issue by nesting requests in success callbacks - i.e. wait for a dependency to load before triggering the next request (old-school and clunky); or you could just use a suitable jquery plugin (for example this); or a module/dependency loader (good background article here).

    As for holding off execution of p5 scripts - I'd suggest using instance mode if you can; since you can choose to execute the sketch code when you want using new p5(sketch)

  • edited December 2016

    @fedpep, I'm glad it's finally worked for ya! O:-)

    But after checking out your "index.html" below:
    https://CodiceInutile.github.io/website-dev/index.html

    I've noticed this comment there: * It currently works only with P5.js script. L-)

    Although, due to p5.js' "Global Mode" intricacies, I've made some hacks to conditionally deal w/ it, AFAIK those tricks shouldn't get in the way of loading other libraries besides p5.js! :-@

  • edited December 2016

    As for holding off execution of p5 scripts - I'd suggest using instance mode if you can;

    @blindfish, I don't think it is @fedpep who controls how the loaded JS programs are coded: 8-|

    A lot of people will contribute on different javascript sketches that will be displayed on the home page: some of them will be written with P5.js, other with other libraries.

    As for dynamically loading scripts in a specific order, function loadOrdered()'s already solved that.
    It's just 1 single statement! No need for more extra libraries or patches besides jQuery: :)>-

    $.getScript(scripts.shift(), scripts.length && loadOrdered || sketchLoaded);

Sign In or Register to comment.