Open araid opened 1 month ago
I think p5 instance mode already lets you do that pretty much: https://editor.p5js.org/davepagurek/sketches/RLPK_J382
The one difference is that it seems to require that you pass a function into the p5 constructor currently, even if it's empty. So maybe the only thing needed to get what you're describing is the ability to use the p5 constructor with no arguments, with the expectation that it will be assigned manually afterwards?
Thanks @davepagurek, this is great. I didn't realize you could define setup
and draw
outside of new p5()
.
Would this still work in a modular architecture though? If we imagine this code split into 2 modulesmain.js
and rectangle.js
, the q5 or sketch variables wouldn't be visible by the rectangle class.
This is of course assuming p5 v2.0 will support ESM. Please correct me if I'm wrong.
Would this still work in a modular architecture though? If we imagine this code split into 2 modules
main.js
andrectangle.js
, the q5 or sketch variables wouldn't be visible by the rectangle class.
That's true, but also is a problem in q5.js too -- you still would have to give the namespace/instance to the class somehow if it isn't able to grab it from the local scope.
A kind of hack you could use is to declare an empty local variable in the Rectangle
file, and export a function to supply the real thing:
let p5 = undefined
class Rectangle {
draw() {
p5.fill(255);
p5.rect(50, 50, 50, 50);
}
}
Rectangle.setup = function(instance) {
p5 = instance;
};
and then in your main sketch:
import { Rectangle } from './Rectangle.mjs';
let q5 = new p5(() => {});
Rectangle.setup(q5); // Pass in the instance here
let rectangle;
q5.setup = function(){
q5.createCanvas(100,100);
rectangle = new Rectangle();
}
q5.draw = function(){
q5.background(0);
rectangle.draw();
}
or, to make one global namespace, have a file like this:
export const q5 = new p5(() => {});
and then import that in all your other files:
import { q5 } from './global.mjs'
class Rectangle {
draw() {
q5.fill(255);
q5.rect(50, 50, 50, 50);
}
}
import { q5 } from './global.mjs'
import { Rectangle } from './Rectangle.mjs';
let rectangle;
q5.setup = function(){
q5.createCanvas(100,100);
rectangle = new Rectangle();
}
q5.draw = function(){
q5.background(0);
rectangle.draw();
}
@araid 2.0 will support ESM and due to the nature of ESM, sketches have to be written in instance mode. The way we are testing and iterating in 2.0 development is using an ESM sketch.
I would recommend the Rectangle
class to have a reference to the p5 sketch instance from its constructor instead of having it in the class's environment somehow as it increase the flexibility of the Rectangle
class being able to be used in multiple instances of p5, and stick closer to the composition pattern of objects.
Thanks, this all makes sense. @davepagurek the solution of creating the sketch in a global file seems the closest to what I'm proposing.
@limzykenneth imho there's a difference in terms of readability and being able to reuse modules, between the current "instance mode" and a potential "namespaced" or "library" mode:
Using this simple THREE.js example as reference: if you look at box.js
it's clear that the only dependency is the THREE library, and the class can be reused in any project that includes that library.
With the format you're suggesting, Box receives the THREE object (or sketch for p5) in its constructor. This to me feels more ambiguous, especially in files that import other dependencies:
import GUI from 'lil-gui';
import Stats from './lib/stats.module.js';
// import p5 from 'p5'; // ideally
class Rectangle { constructor(p5) { // currently this.p5 = p5; } ...
I still believe supporting this architecture would be valuable to people looking to integrate p5.js in larger apps along other libraries, but I'm not familiar enough with the codebase to estimate the complexity of building it. I'd love for the feature to be considered for 2.0 but feel free to close the issue if it doesn't sound feasible.
Thanks again!
@limzykenneth I think we don't actually need anything too heavy to support this. I think to fully allow sketches to be declared outside of a sketch callback in instance mode, we'd need to:
sketch
in to new p5()
(maybe have a different signature that we use internally to start global mode?)_start()
is only called on the window load
event, giving user code a chance to attach setup
and draw
to the instance. But if it's initialized when the page has already loaded, to make sure setup happens after user code has had a chance to run, I think we'd just need to defer start by doing Promise.resolve().then(() => this._start())
to stick it at the end of the execution queue.@davepagurek
not assume that we're in global mode if you don't pass sketch in to new p5() (maybe have a different signature that we use internally to start global mode?)
The existing mechanics is that to defer global mode initialization p5.instance
is set to not null
then when ready new p5()
is called to initiate global mode. If we were to change default behavior of calling new p5()
, this mechanic will need to change as well.
if everything's loaded in the head tag, it's already sufficient since p5's _start() is only called on the window load event, giving user code a chance to attach setup and draw to the instance. But if it's initialized when the page has already loaded, to make sure setup happens after user code has had a chance to run, I think we'd just need to defer start by doing Promise.resolve().then(() => this._start()) to stick it at the end of the execution queue.
This I feel have a bit too much assumption in that it only works as part of a script tag include, not with ESM. It's also a bit hard to know how much to defer initialization if for example the user code attaches to setup and draw aynchronously.
@araid For Three.js they work quite differently from p5.js in that Three.js does not provide a runtime and so they don't have an initialization, meaning it doesn't matter the order in which things are defined for the most part, also they use a very different mostly OOP based API whereas p5.js are almost entirely procedural.
The syntax @davepagurek suggested will work if you wish to expose the p5 instance to the environment instead of having it be owned by the object, it is just my opinion that attaching the instance as a class member is the better approach in terms of reusability and flexibility.
import p5 from 'p5'; // ideally
imports the constructor, not the instance so I'm not sure how that will work though.
@davepagurek Thinking out loud a bit, I think what we can do/look into instead if we want to enable something like namespaced mode, we can look into the initialization step in init.js
. init.js
is exclusively run for global mode, if no window attached setup()
or draw()
exists, it already will not start global mode initialization.
In ESM instance mode, init.js
will basically never run so we can try to split things based on that. Let me have a think first.
We make the following assumptions for anyone wishing to use namespace mode:
init.js
(more restrictive, ESM don't need this assumption and can still apply below)setup()
nor draw()
so global mode won't be triggerredp5
instance with const p = new p5()
setup()
, draw()
, and whatever else to p
A potential way going back to @davepagurek's suggestion of Promise.resolve().then(() => this._start())
, we instead indefinitely defer it until this.setup
or this.draw
is defined:
const repeatUntilInit = () => {
if(this.setup) {
this._start();
}else{
setTimeout(repeatUntilInit);
}
}
repeatUntilInit();
Not sure if this breaks anything though.
This I feel have a bit too much assumption in that it only works as part of a script tag include, not with ESM. It's also a bit hard to know how much to defer initialization if for example the user code attaches to setup and draw aynchronously.
I guess we already don't support asynchronously attaching setup/draw in the currently recommended initialization, right?
const sketch = new p5(async (p) => {
await new Promise((res) => setTimeout(res, 5000))
p.setup = () => p.createCanvas(200, 200)
p.draw = () => p.background('red')
})
Given that, I think it's not too much of a stretch to tell users that if they want to add setup/draw externally, it would also need to be done synchronously after construction, but I guess that means just doing new p5()
in one file and attaching setup and draw in another file wouldn't be supported because it wouldn't reliably be synchronous.
A potential way going back to @davepagurek's suggestion of
Promise.resolve().then(() => this._start())
, we instead indefinitely defer it untilthis.setup
orthis.draw
is defined
That looks like it could work, or also assigning setters on the setup
and draw
properties of the sketch so that when they're modified, we can immediately start initialization?
class p5 {
// ...
set setup(cb) {
this._setup = cb
this.start()
}
}
...or one other option is to just expose a .start()
that you have to manually call if you don't immediately pass a sketch function into the constructor.
I like the setter option but it may have problem where setup is defined and the runtime start but draw is not yet defined. We can try to add a setTimeout
defer to that if setup and draw are in the same event loop cycle it will be available when the runtime starts, it doesn't solve the problem of asynchronously attached setup and draw or any other user defined functions but it probably is an ok tradeoff, given realistically there probably isn't a possible start time if we make that assumption.
Calling a .start()
manually is pretty close to the current API of calling the constructor after defining the sketch so I feel it's not adding too much value here.
I can look into the code and implementation after I'm done with the refactoring but in any case the priority is to make sure global mode, instance mode, and possibly defer global mode we currently have work the same way otherwise we could be breaking a huge amount of existing tutorials out there.
Makes sense! Yeah I consider this mostly a nice-to-have if we've got time
Increasing access
unsure
Most appropriate sub-area of p5.js?
Feature request details
The current global and instance modes are not ideal for integrating p5 into larger web applications:
sketch
object to every class that wants to use the p5 library, making the code more complex.It would be great for 2.0 to add a third instantiation mode that's more in line with how THREE, Babylon or other libraries work.
In this mode, p5 functions would be neatly contained in a single global object, getting us closer to the modular approach that's already underway. Lingdong explains it very well in his q5xjs library.
So instead of doing this:
we would do this:
which seems like a very small gain but can lead to much more readable code as the application grows.