processing / p5.js

p5.js is a client-side JS platform that empowers artists, designers, students, and anyone to learn to code and express themselves creatively on the web. It is based on the core principles of Processing. http://twitter.com/p5xjs —
http://p5js.org/
GNU Lesser General Public License v2.1
21.68k stars 3.33k forks source link

async issues (loadStrings loadJSON, loadImage) #81

Closed lmccart closed 11 years ago

lmccart commented 11 years ago

two options:

var data; loadJSON("file.json", data);

var data; loadJSON("file.json", function(resp) { data = resp; });

evhan55 commented 11 years ago

Promises: http://blog.parse.com/2013/01/29/whats-so-great-about-javascript-promises/

iros commented 11 years ago

Can the load json function be async? Because in that case the callback option is more versatile.

On Oct 22, 2013, at 2:44 PM, Lauren McCarthy notifications@github.com wrote:

two options for redesign:

var data; loadJSON("file.json", data);

var data; loadJSON("file.json", function(resp) { data = resp; });

— Reply to this email directly or view it on GitHubhttps://github.com/lmccart/processing-js/issues/81 .

lmccart commented 11 years ago

we could support both. simple version for beginners, callbacks for those that want to use them!

taseenb commented 11 years ago

i think that the second option should be definitely supported (and a Promise-like option even too much!).

the first one looks ok for absolute js beginners, but is confusing too. a sketch could simply not work (or work in unexpected ways), even if it was "officially" correct. this can be even more frustrating for a beginner, than learning how things really ARE in the wonderful async world..

inspired by the solution used in Processing.js, i was thinking that beginners could be invited to define a list of files to load at initialization: within setup() or before, in a dedicated function like preload() (?). this kind of solution should limit unexpected behaviors for async operations and keep things simple.

// i'm totally ignoring globals/scope problems in this example

function preload() {
    var data = loadJSON("file.json");
    var xml = loadXML("file.xml");
    var img = loadImage("file.jpeg");
    var txt = loadStrings("file.txt");
    var anyfile = load("http://www.google.com/");
}

setup() {

}

draw() {

}
shiffman commented 11 years ago

Sorry for my totally naive question, but what would be the difference between having preload() and calling those functions in setup() itself? I love, btw, the option of having non-async versions for loading at the beginning and fancier async versions for advanced.

lmccart commented 11 years ago

I was wondering this also. I guess the case would be when you want to do 3 things in order. 1. Load files, 2. use data to initialize variables, 3. draw using initialized variables.

Not sure this warrants an extra preload method though, maybe this is a use case where you'd use a callback function, loading the files in setup.

brysonian commented 11 years ago

My understanding is that the idea behind the preload method is that it would behind the scenes treat setup as the callback. So when everything in preload was done, setup would fire, then draw etc.

Maybe i've just been coding javascript for too long, but despite the ease of learning, i think sync versions of those functions will cause more harm than good. The whole page will freeze while waiting, and that wait could be a long time depending on the network and the size of the asset. Perhaps another route is to set a field on the var to true when the data is available?

var myImage;

function setup() {
  myImage = loadImage("foo.jpg");
}

function draw() {
  if (myImage.available) {
    // do stuff with myImage
  }
}

It is a little less straightforward for json and strings because once those are ready you'd probably want to init some stuff just one time. But since those functions are exactly for beginners, perhaps it is ok to force them to either specify a callback, or use a built in one ala movieEvent in the Movie lib.

taseenb commented 11 years ago

@shiffman and @lmccart I don't know how you want to stop code execution while you load files in the "beginner way". Am i missing something? Javascript doesn't wait and you'll have to stop it somehow. So if the user wants to use data from the files in setup already, i don't see how it would work exactly without a callback.

@brysonian Promise-like solution looks very easy to use actually. You are right: the problem is again that files loaded in setup won't be ready until draw is called. Concerning the user experience (and to avoid panic), probably a loading icon should be shown, while the application is "freezed".

brysonian commented 11 years ago

You can make sync requests with XMLHttpRequest, but it is frowned upon.

taseenb commented 11 years ago

True, this could be used in setup() actually. Unintended things could happen though, like the application freezing in the middle of something, but probably this is not too bad, compared to the advantage of simplicity.

fjenett commented 11 years ago

Processing.js solved this by introducing something similar to compiler-flags called directives: http://processingjs.org/reference/preload/

These are parsed out by the precompiler and the file(s) are loaded before the sketch is started. I'm not much infavour of these and as we don't have a precompiler they are off the table anyways i guess ..

Are there any plans for a build system? .. if so this problem could easily be fixed by turning the data into js files and have them be "loaded" that way:

// in fancy.json.js which is generated by build system from file fancy.json
var preload = preload || {};
preload['fancy.json'] = '{"my":"json","string":"here"}';
// then somewhere in the sketch
loadJSON('fancy.json'); // first searches preload['fancy.json'], otherwise attempts to load file ...

This is how brunch.io does it with it's require() statements for template data ... they even concatenate all generated js "files" into one large "database" for us this could be:

// build a virtual file system inside the object:
var preload = preload || {};
preload['data/first.json'] = '...';
preload['data/other.json'] = '...';
preload['other-dir/other_file.json'] = '...';
taseenb commented 11 years ago

@fjennet probably a very stupid question: the build system you describe works in a browser? What's the difference between the build system and a sync or async request made at any time in a sketch? I am missing something.

fjenett commented 11 years ago

No, good question. A build / assembly system would run before you push files onto your server (FTP) or as part of a preview step (maybe in the PDE). For example when you play a sketch in the JavaScript mode in the Processing PDE before the project opens in a browser files will be copied together and precompiled, etc. Another example would be something like brunch.io that does something similar (assembles files, concatenates and minifies them and packages the project).

So the difference then would be that the content to load would / could already be loaded at the moment any of the loadXXX() methods is being run. ... and yes, that would only work if you know which files are being loaded at the time the project is assembled. Does not work for external resources for example.

taseenb commented 11 years ago

Ok, thank you for the explanation. I see now what you mean. Interesting to share optimized standalone applications made in js. But as you say it won't work with external resources loaded during runtime.

yyx990803 commented 11 years ago

The simple version won't work because when you do loadJSON("file.json", data) you are passing in undefined and you won't be able to modify the external data reference from within loadJSON().

Here are some thoughts regarding each proposal so far:

fjenett commented 11 years ago

RE: build step ...

Imagine there is something like the current "data" folder ("data-preload"?) and all files in there will be automatically preloaded before your sketch is running (build tool just loops files and generates a preload-list). Then even dynamic names will work and you would not have to write them (any) out.

I think tooling is needed anyways. No?

lmccart commented 11 years ago

@fjenett ooh this is a nice idea

yyx990803 commented 11 years ago

@fjenett this would require access to local file system to scan what files exist, so it has to be a legit build step (like setup build/watch via grunt) instead of a runtime thing (save -> refresh). Personally I have no problem with a build step, but that might be too much for a beginner to jump through just to load an external file, unless you have a simpler workflow in mind.

fjenett commented 11 years ago

@yyx990803 yes you would need file access.

I think for beginners it needs something like the PDE or similar that will allow for easy sketch creation / export. So with this in place it would be super easy to add that little extra build step. ... i'm not talking about grunt or similar here, but rather a really simple application that only creates / lists / runs and exports p5.js sketches, not a full editor.

yyx990803 commented 11 years ago

@fjenett

I see, that make sense - so I guess for those who use their own editor we can assume they are capable of manually handling callbacks?

taseenb commented 11 years ago

Mmm. I wonder a few things about the goals:

  1. The Java version of Processing does it perfectly: it blocks code execution and waits. Why should I learn js and use Pjs if I simply need/want that feature as it is already available today in the desktop environment? What would I get?
  2. An IDE (desktop app and browser app) + any solution to help building, optimizing and sharing a pjs sketch are and would be nice features or add-ons to this project. But should we rely on those from the API? Or should we make the API beginner-ready without any "extra" tool (but a browser, of course)?
  3. Js is a scripting language that may be simply written in a textarea and run in a simple static HTML page. Nothing could be simpler for someone that never saw a line of code, I think: you write and click 'play'. Why should a beginner be forced to code in js using a desktop IDE and building an application that would play in a browser, if he simply wants to load a file?
yyx990803 commented 11 years ago

Personally, I think what makes web technology great is that it's an open standard and you can author in any environment/editor you want, and your shipped code will run in any modern browser. I feel that binding the library to a specific IDE takes that advantage away. That's why I'm all for the run-time parsing: it allows the user to write in whatever editor they like and not rely on any additional tools but the browser.

fjenett commented 11 years ago

I don't think making an IDE (again) is a good idea and i was not talking about that. What i had in mind was a simple tool that would:

No more no less. Something like the JavaScript mode as standalone.

But ... as this is getting far away from the original async question and issue we should maybe move some place else for discussion?

taseenb commented 11 years ago

Yes sorry, we moved away from the question. The build tool is a big subject, since js is evolving quickly and we could probably even imagine to build html5 apps, but also desktop node-WebKit apps or mobile apps with pjs one day... Don't know, just absurd ideas maybe. Anyway it definitely deserves a separate discussion, since it is not directly related to the API and we risk to mix too many things.

taseenb commented 11 years ago

@yyx990803 Could you explain how you imagine the "run-time parser" of a sketch? Probably I'm naive and did not understand something, but wouldn't it be too complex to code and heavy to maintain? What I understand is that you would first load the sketch as a string, find the "loadFile" functions (json, image, xml, whatever), get the requested files, apply their values somehow and finally execute the sketch by calling setup(), etc... Is that right?

For example, would it work in a situation like this?

var file = [];

for (var i=0; i<5; i++) {
    file[i] = loadJSON("data" + i + ".json");
}
yyx990803 commented 11 years ago

@taseenb basically, yes. It could be as simple as a RegExp match, but cannot deal with the situation you mentioned (dynamic filenames).

taseenb commented 11 years ago

I see. Still think that a preload function could be the simplest: simple for the users (and not too hard for the developers to implement and maintain), and still powerful enough in most cases (dynamic filenames could work). Callbacks (maybe using @brysonian idea: https://github.com/lmccart/processing-js/issues/81#issuecomment-27189486 ) could be used by advanced users, but the two options (callbacks and preload) would be consistent and the behavior of the API easily predictable.

var file = [], xml, img;

function preload() {

    // here you are free to do whatever you want - not only call the load functions...

    var rnd = parseInt(Math.random() *1000);    
    xml = loadXml('file' + rnd + '.xml');

    for (var i=0; i<5; i++) {
        file[i] = loadJSON("data" + i + ".json");
    }

    img = loadImage('pic.jpg');
}
fjenett commented 11 years ago

Yes to callbacks and the preload() from my side. Although i think the preload should work differently. It only should return a list of items to preload, but not allow to already start working with the data (API should maybe not be available?). That is because without synchronous call returns setting variables like above won't work (without using promises and the like) and if it would there would be no need for the preload.

preload ( files ) {

    for ( var i = 0; i < 100; i++ ) {
        files.push( "fancy-"+i+".json" );
    }

    return files;
}

The resulting array could easily be merged with other resources (maybe from the build step) that need to be loaded, there could be something like a progress callback ... and proper error reporting.

Bringing up error reporting, there should be two callbacks: loadXXX( url, success, error)

taseenb commented 11 years ago

Yes, sorry: that way wouldn't work. But if you simply use a string to set the name of the global variable, it would, almost exactly the same way:

var text;

function preload() {
    load('text.txt', 'text');
}
fjenett commented 11 years ago

Hm, that would be an addition to the API then ... no? I think the objective was to decide on how to handle the loadXXX() methods so that they behave similar to the Java version.

yyx990803 commented 11 years ago

I like @fjenett 's preload method because I don't think it's a good idea to limit preload data to global variables only. I'm not sure why we need to pass in files as an argument though - simply returning an array seems to suffice.

lmccart commented 11 years ago

I think I'm in favor of something like:

var data, img;

function preload() {
     loadJSON("file.json", data);
     loadImage("cat.jpg", img);
}

Maybe only loadXXX methods would work in here. This would work if we wrote the load methods right, wouldn't it?

yyx990803 commented 11 years ago

@lmccart the data and img you are passing in here are both undefined at the time the functions are called, so you won't get a reference to the actual variables defined outside the function. If you pass in strings instead, e.g. "data", then you can do window["data"] = ..., but that would force all preload variables to be global and in general is not a good idea.

taseenb commented 11 years ago

@yyx990803 is right. Variables cannot be passed because javascript does not allow to pass them by reference... We should pass a string with the name of the variables...

Check this working Gist (very rough): https://gist.github.com/taseenb/7284649

var text;

function preload() {
    load('text.txt', 'text');
}

function setup() {
    console.log(text);
}

Any ideas on how to avoid only globals or allow arrays, in a simple way for the user?

taseenb commented 11 years ago

@fjenett It does not look a big addition. It's pretty simple. No?

lmccart commented 11 years ago

oh sorry, I wasn't thinking. @taseenb I didn't quite understand why your earlier example wouldn't work though? https://github.com/lmccart/processing-js/issues/81#issuecomment-27630235

couldn't we do something like a manual promises...counting up each load call and not proceeding to setup until all things have loaded? this just seems most straightforward, rather than having to pass a string with the var name. but maybe I'm missing something?

taseenb commented 11 years ago

@lmccart That example does not work because there is no way to get a reference to those variables. We don't know what the user is doing inside preload(): we can just determine whether preload() exists (and is a function) and if the user has called the load() function. But we don't know the name of the variables, so we don't know where to put the data being loaded.

This one works though: https://github.com/lmccart/processing-js/issues/81#issuecomment-27635147 (it's not exactly the same, but probably the closest).

taseenb commented 11 years ago

@yyx990803 You are totally right about globals. But at the moment everything is global. This will probably change in the future.

yyx990803 commented 11 years ago

@taseenb yes - I actually really don't like exposing everything globally as that encourage bad practice for JavaScript beginners and introduces potential naming clashes when using Pjs with other libraries.

lmccart commented 11 years ago

@taseenb @yyx990803 yes definitely agree! the plan is to switch away from that. or at least to offer two options so beginners may use global namespace but you can have the option to use scoped namespace instead.

@taseenb sorry, I'm still confused. why do we need a reference to the variables in your earlier example? couldn't we increment a count with each loadimage or loadjson call and decrement on each return? then go on to setup once count is at 0 again?

yyx990803 commented 11 years ago

@lmccart consider this:

User's code:

var data = load('file.json')

Library code:

function load (file) {
    // do the XHR request here...
    xhr.onload = function () {
        // because we have no idea what variable name
        // the user used, how can we assign the loaded value?
    }
    // what do we return here? we don't have anything yet.
    // the user expects actual data, not a promise.
}
yyx990803 commented 11 years ago

On the other hand - if we do this:

User code:

var a, b

function preload () {
    return ['a.json', 'b.json', 'c.json']
}

function setup () {
    a = loadJSON('a.json')
    b = loadJSON('b.json')
    // local variable works too
    var c = loadJSON('c.json')
}

Library code:

var files = preload(),
    total = files.length,
    preloadedFiles = {}

files.forEach(function (file) {
    // do xhr request ...
    xhr.onload = function () {
        // implementation here depends on file extension
        preloadedFiles[file] = JSON.parse(xhr.responseText)
        total--
        if (total === 0) {
            // we are ready
            setup()
        }
    }
})

function loadJSON (file) {
    return preloadedFiles[file]
}
taseenb commented 11 years ago

@lmccart In the gist https://gist.github.com/taseenb/7284649 this is exactly what i do: count each load() and wait until they are finished. This is ok.

But the problem comes with variable names set by the user: how do we get those names and put the right data into the right variable? Not simple. Of course, you could get the body of preload() and use regular expressions to find variable names (something close to what @yyx990803 suggested some time ago): i tried it, but in my opinion it wasn't nice. The cleanest code i found that works for sure, for the moment, is the one passing the variable names as strings. Only for the moment!

taseenb commented 11 years ago

@yyx990803 That looks VERY clean. Very similar to the last idea of @fjenett but only now I understand it.

To be even simpler and avoid that anti-beginner return it could also be:

preload = ['a.json', 'b.json', 'c.json'];

var a, b, c;
function setup() {
    a = loadJSON('a.json')
    b = loadJSON('b.json')
    c = loadJSON('c.json')
}
brysonian commented 11 years ago

perhaps

preload('a.json', 'b.json', 'c.json');

Better expresses that this is an action (function call) whereas the preload array variable feels a bit magical.

Another option is that the user defines a preload function if they want it:

function preload() {
    var dataA = loadJSON('a.json');
    var dataB = loadJSON('a.json');
}

And the runner in P5.js on init:

looks to see if that function is defined, if so loadJSON is set to a preload version which: incrememts a counter for the number of preloads required returns a P5JS.JSONObject instance with a callback set to an internal method that counts successful preloads when the count is reached calls setup() else loadJSON is set to the "normal" version which requires that the user define a callback herself.

The java version of processing returns an object from loadJSONObject and loadJSONArray with getters for properties:

JSONObject json = loadJSONObject("data.json");
int id = json.getInt("id");
String species = json.getString("species");
String name = json.getString("name");

So the P5JS.JSONObject would behave the same way. Perhaps with an added getter for the raw JS data structure. This technique avoids the jiggery-pokery of parsing the function body as a string and allows for code-generated file and variable names.

fjenett commented 11 years ago

... i think it would be very confusing for beginners if the loadXYZ() methods would behave differently in different locations.

Returning an array from preload would

@yyx990803 ... files argument so you don't have to create that yourself as beginner. For convenience.

The question of what the loadXYZ return is another thing and i think not related to the preload / async question. My view is that the purpose of this project is to assess JS as a main language and therefore i don't see why we should mimic Java here.

brysonian commented 11 years ago

But what you are proposing also has the loadXXX functions behaving differently too. In your case if preload has returned a list of files, those have been loaded and stuffed into an array that mimics a file system, and subsequent calls to the loadXXX functions then look in that array for the data. So if my preload() was:

function preload() {
  return ['a.json', 'thumbnail.jpg'];
}

And then i had:

var thumb;
var data;
var otherImg;
function setup() {
  data = loadJSON('a.json');
  thumb = loadImage('thumbnail.jpg');
  otherImg = loadImage('other.jpg');
}

So calls to loadImage would behave differently in the same place which is even more confusing.

My suggestion is that the API make the promise that any async calls in the preload function are guaranteed to have completed by the time setup is called, that is all.

taseenb commented 11 years ago

Could you guys write a simple working gist of your propositions, if you have a little time? I think that would be of great help to see how it would work.

brysonian commented 11 years ago

Yes absolutely. I'm tied up until this evening. But I'll put it together then.

fjenett commented 11 years ago

@brysonian i don't think it' confusing because for otherImg that would return null and eventually throw an error. If you want to use additional resources that you did not state in preload() then you should use requestXYZ(): http://processing.org/reference/requestImage_.html ... which is there in the API for async loading.

Sorry, no time to work on an example of this, at least not any time soon.