Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[p5.js 2.0 RFC Proposal]: New third party library authoring API #7015

Open
2 of 21 tasks
limzykenneth opened this issue May 4, 2024 · 5 comments · Fixed by #7203
Open
2 of 21 tasks

[p5.js 2.0 RFC Proposal]: New third party library authoring API #7015

limzykenneth opened this issue May 4, 2024 · 5 comments · Fixed by #7203
Assignees

Comments

@limzykenneth
Copy link
Member

Increasing access

Addon libraries have always been an important part of the p5.js ecosystem, expanding its capabilities with community contributed features that solve problems that p5.js itself may not necessarily address. Providing a flexible and easy to use interface to author addon libraries will further this effort.

Which types of changes would be made?

  • Breaking change (Add-on libraries or sketches will work differently even if their code stays the same.)
  • Systemic change (Many features or contributor workflows will be affected.)
  • Overdue change (Modifications will be made that have been desirable for a long time.)
  • Unsure (The community can help to determine the type of change.)

Most appropriate sub-area of p5.js?

  • Accessibility
  • Color
  • Core/Environment/Rendering
  • Data
  • DOM
  • Events
  • Image
  • IO
  • Math
  • Typography
  • Utilities
  • WebGL
  • Build process
  • Unit testing
  • Internationalization
  • Friendly errors
  • Other (specify if possible)

What's the problem?

Currently to author an addon library to work with p5.js, the library author will often need to attach methods directly to p5.prototype which by itself is not a bad idea but addon libraries often need to do more than that.

While async/await setup() proposed in #6767 provides a potentially new way of handling async functions, current libraries that need to hook into preload() require the use of internal functions.

The lifecycle hooks feature of the current addon library API is also not entire consistent with room for improvement. Finally with a new syntax, there is room for even more customizability, combined with #7014, one can even add or overwrite renderers available to p5.js.

What's the solution?

Existing libraries should have a level of compatibility or require minimal updates to work with p5.js 2.0. This means if existing libraries rely on attaching methods to p5.prototype it will likely still work.

A new method of authoring libraries will be introduced that is more ergonomic. This will be through a factory function that exposes reasonable interfaces for completing the following tasks as necessary:

  • Attaching methods and properties to prototype
  • Lifecycle hooks
  • Extending internal functionalities (eg. adding a new renderer)

As reference, Day.js provide plugin interface in the following way:

export default (option, dayjsClass, dayjsFactory) => {
  // extend dayjs()
  // e.g. add dayjs().isSameOrBefore()
  dayjsClass.prototype.isSameOrBefore = function(arguments) {}

  // extend dayjs
  // e.g. add dayjs.utc()
  dayjsFactory.utc = arguments => {}

  // overriding existing API
  // e.g. extend dayjs().format()
  const oldFormat = dayjsClass.prototype.format
  dayjsClass.prototype.format = function(arguments) {
    // original format result
    const result = oldFormat.bind(this)(arguments)
    // return modified result
  }
}

And is used with:

dayjs.extend(myPlugin);

While jQuery provides the following interface:

$.fn.greenify = function() {
  this.css('color', 'green');
};

$('a').greenify();

fn above is just an alias to prototype which in essense makes jQuery's plugin system identical to what p5.js does.

p5.js plugins have some properties that are not present in the Day.js or jQuery use case. With Day.js, plugins are expected to be explicitly provided through dayjs.extend() while p5.js addons should have the expectations of being available immediately upon inclusion/load. jQuery plugin don't need to content with lifecycle hooks or other non-class instance related features. A p5.js addon should also have the flexibility of being imported as a ES module or included through a script tag, ie. there should be a ES module version and a UMD version ideally.

The proposed interface that a p5.js 2.0 plugin can have is as the following:

(function(p5){
  p5.registerAddon((p5, fn, lifecycles) => {
    // `fn` being the prototype
    fn.myMethod = function(){
      // Perform some tasks
    };

    // Instead of requiring register preload,
    // async/await is preferred instead.
    fn.loadMyData = async function(){
      // Load some data asynchronously
    };

    lifecycles.presetup = function(){
      // Run actions before `setup()` runs
    };

    lifecycles.postdraw = function(){
      // Run actions after `draw()` runs
    };
  });
})(p5);

Pros (updated based on community comments)

  • User friendly and flexible API to extend p5.js through addon libraries
  • Unified interface for authoring libraries and working on internal modules, if you are a library author, you will also know how to work on p5.js internals

Cons (updated based on community comments)

  • There is some risk of breaking existing libraries, we will try to test with many existing libraries and update libraries authors where necessary.

Proposal status

Under review

@mvicky2592
Copy link

mvicky2592 commented Jun 16, 2024

@limzykenneth I like the idea but it's a bit unclear from your example code how this would achieve the goals of esm support for example. Could you elaborate on that and show an example?

Also I don't understand the purpose of using an anonymous function, the first and last lines in your example.

p5.js users might be loading multiple libraries and the current implementation of p5.prototype.registerMethod already supports multiple libraries hooking into init, preload, setup, pre and post draw quite well with more standard and familiar JavaScript event style syntax. How could lifecycles support this? Also the syntax seems more similar to deprecated onclick functions, the old way of handling events in js.

I'm also confused how library creators could access a p5 instance.

@limzykenneth limzykenneth self-assigned this Jun 18, 2024
@humanbydefinition
Copy link

humanbydefinition commented Jul 15, 2024

Hey everyone, I got asked by @davepagurek to add context to this, since I am currently working on an addon library for p5.js myself and ran into some minor issues where it would be great if a fix is provided for the upcoming 2.0 update.

The addon library I am working on renders the p5 canvas as ASCII art, for which the user can upload their own font to be used by using loadAsciiFont() in the sketches preload() function. In case the user does not provide this line of code inside the preload() function, I want to provide a default font to be loaded, which I am doing using a beforePreload-hook.

Doing that didn't work immediately and I figured that I have to manually call this._incrementPreload() before loading the font and this._decrementPreload() after it is actually done loading.

The relevant code from my work-in-progress addon library looks like this:

p5.prototype.loadDefaultAsciiFont = function () {
    this._incrementPreload();

    P5Asciify.font = this.loadFont(URSAFONT_BASE64, () => {
        this._decrementPreload();
    });
};
p5.prototype.registerMethod("beforePreload", p5.prototype.loadDefaultAsciiFont);

p5.prototype.loadAsciiFont = function (fontPath) {
    P5Asciify.font = this.loadFont(fontPath, () => {
        this._decrementPreload();
    });
};
p5.prototype.registerPreloadMethod('loadAsciiFont', p5.prototype);

Cheers!

@davepagurek
Copy link
Contributor

Thanks @humanbydefinition! For the 2.0 API, this could mean changing when the beforePreload and afterPreload hooks run so that an addon's loadFont will still be awaited (depending of course on what we decide to do about async/await in general.)

@davepagurek
Copy link
Contributor

davepagurek commented Jul 15, 2024

Also to shed more clarity on how lifecycles maps to registerMethod, you can think of lifecycles as a shortcut: If you set lifecycles.beforeDraw when setting up your addon, then it's like p5.prototype.registerMethod('beforeDraw', lifecycles.beforeDraw) gets called for you afterwards. (That is to say: each addon gets its own lifecycles object to add to.) The motivation for the syntactic change, in my mind anyway, is to make writing an addon look more similar to writing a sketch, and similar to how a sketch has just one setup and draw, an addon would just specify one function for each lifecycle hook that it wants to listen to as well.

@limzykenneth do you have thoughts on whether or not the registerMethod function will still be available, e.g. for backwards compatibility?

@limzykenneth
Copy link
Member Author

@humanbydefinition For the proposed async/await implementation with the new library API and your use case you can detect if loadAsciiFont() was called in a postsetup lifecycle hook (which is essentially a function you defined) and if it was not called, you can proceed with loading the default ASCII font. The lifecycle functions will be awaited so you can load the fonts asynchronously as well.

Essentially how the lifecycles defined by libraries work is that everytime p5.registerAddon() was called with a defined lifecycle object with relevant functions in them, each of them will be added to an internal array (eg. postSetupHooks = [hookFromAddon1, hookFromAddon2, ...]) then each will get executed with an await at the relevant points (not sure if they will be run concurrently or not, still need to figure this detail out).

let fontLoaded = false;

p5.registerAddon((p5, fn, lifecycles) => {
  // `fn` being the prototype
  fn.loadAsciiFont = async function(font){
    // Load user defined font
    fontLoaded = true;
  };

  fn.loadDefaultAsciiFont = async function(){
    // Load default font
  };

  lifecycles.postsetup = async function(){
    // Run actions after `setup()` runs
    if(fontLoaded === false){
      await fn.loadDefaultAsciiFont();
      // or maybe `await this.loadDefaultAsciiFont();` still need to figure out `this` binding
    }
  };
});

Somewhat like the example code above, but probably with a different state management arrangement depending on how you'd design your API.

@davepagurek My current plan is to not retain the registerMethod() related stuff (or at most retaining it but deprecating it with a planned removal date/version), the registerPreloadMethod() didn't work in the first place so it won't be retained. _incrementPreload() and _decrementPreload() are meant to be private internal API which by design shouldn't be used but needed to get around registerPreloadMethod() not working as intended. One of the plan is to provide a compatibility library for those still transitioning from 1.x to 2.0 so that 2.0 matches 1.x functionality where possible with some extra code. This compatibility library will only last for a short while though so we don't maintain two different APIs indefinitely.

I will be testing this part the the feature/changes with most if not all external libraries and provide fixes where needed. Mostly libraries that attaches to prototype will likely still work (bar changes we made elsewhere in the core) but those that uses preload will likely need updating.

@limzykenneth limzykenneth linked a pull request Sep 12, 2024 that will close this issue
4 tasks
@limzykenneth limzykenneth moved this from Implementation to Completed in p5.js 2.0 Nov 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Completed
Development

Successfully merging a pull request may close this issue.

4 participants