Designing an addon library system for p5.js 2.0

TLDR: If you have an existing addon library, test if it already works with p5.js 2.0, you may not need to make any changes. If there is something that isn't working, go through the flow below to troubleshoot and find a solution: p5.registerMethod not found error message Instead of using p5.registerMethod, use the new addon library syntax for lifecycle hooks. // Before p5.prototype.setDefaultBackground = function(){ this.background("#ed225d"); }; p5.prototype.registerMethod("pre", p5.prototype.setDefaultBackground); /////////////////////////////// // After const myAddon = function(p5, fn, lifecycles){ lifecycles.presetup = function(){ this.background("#ed225d"); }; }; p5.registerAddon(myAddon); p5.registerPreloadMethod, _incrementPreload, _decrementPreload not found error message Consider switching to returning promise from your asynchronous function, read here for possible transition methods. Anything else not working as expected File a bug report with p5.js p5.js 1.x will still receive bug fixes for at least a year while p5.js 2.0 is being rolled out. Please read the final section of this article for some transition strategy your library might want to take and do contact the p5.js team if you need help or clarifications on your library's transitions. Part 1 Addons, plugins, extensions, or any other name they may be called, these are just different names to a very common concept in the Javascript ecosystem. From the years where all our project starts with jQuery, to now where modern frameworks, libraries, or tools such as Vue, Three.js, and ESLint have some level of support for addons. Even projects that don't explicitly have an interface for addons will often see addons written for it to expand on its functionalities. That last point, expanding functionalities, is exactly the idea we are going to explore here. Unlike a compiled language such as C, C++, Rust, or Java, Javascript is not served to the end user as binary, it is by design served as text. Also unlike other non-compiled language, Javascript is often served frequently over the internet, instead of being packaged and downloaded by the user once to be run multiple time, such as what you might do with any other applications on your computer. This puts Javascript in a position where it needs to do as much as it needed with as little code as necessary: the more code it has, the slower Javascript gets downloaded, the slower the experience it is for the end user. One method to mitigate this need for small download size is minification and compression. We won't explore that here. The other method that we alluded to is to be as minimal as necessary, to do as much as is necessary and not more. However, it is often a challenge to know what is "necessary". Each library will need to determine that for themselves and we will explore this further in the future, but for now let's take this as a tautology and look at what it means in practice. Once a library decided the necessary functionalities it needs, it gets implemented and people can use it, great! However, there could very well be use cases where people needed a bit more functionalities that are not included, and the library doesn't want to include for various reasons. We don't want our libraries to be able to do everything possible when we are only using a small subset of its capabilities, otherwise our libraries will be excessively bloated. In these kind of cases, it would make sense to create an addon/plugin/extension library that expands on the functionalities of the library. Depending on the design and purpose of the library, there are several different techniques when it comes to expanding functionalities, none are better than others necessarily, and some libraries are only meant to be expanded one way while others were never meant to be expanded at all. This is to say you should see the following as different options or approaches rather than any kind of value judgement on the techniques themselves. Note: I will be using the terms "addon", "plugin", and "extension" interchangeably here depending on context and which term individual library uses. They all mean the same thing in our context. Direct extension Javascript for better or for worse (depending on who you ask) is a pretty flexible language, especially when it comes to its type system. Javascript is a relatively rare object oriented language that allows the modification of a class definition at runtime, ie. it is able to modify the template in which each object is created from in the middle of execution, this is not something that stricter languages will ever allow. Technically, in Javascript we are not modifying the class definition, for the class syntax that you may have come across in Javascript is mostly syntactic sugar (syntax abstraction) of the real underlying object oriented model that Javascript uses which is prototype based object oriented programming. We will leave det

Feb 21, 2025 - 13:22
 0
Designing an addon library system for p5.js 2.0

TLDR: If you have an existing addon library, test if it already works with p5.js 2.0, you may not need to make any changes. If there is something that isn't working, go through the flow below to troubleshoot and find a solution:

  1. p5.registerMethod not found error message

    • Instead of using p5.registerMethod, use the new addon library syntax for lifecycle hooks.
    // Before
    p5.prototype.setDefaultBackground = function(){
      this.background("#ed225d");
    };
    p5.prototype.registerMethod("pre", p5.prototype.setDefaultBackground);
    
    ///////////////////////////////
    
    // After
    const myAddon = function(p5, fn, lifecycles){
      lifecycles.presetup = function(){
        this.background("#ed225d");
      };
    };
    p5.registerAddon(myAddon);
    
  2. p5.registerPreloadMethod, _incrementPreload, _decrementPreload not found error message

    • Consider switching to returning promise from your asynchronous function, read here for possible transition methods.
  3. Anything else not working as expected

p5.js 1.x will still receive bug fixes for at least a year while p5.js 2.0 is being rolled out. Please read the final section of this article for some transition strategy your library might want to take and do contact the p5.js team if you need help or clarifications on your library's transitions.

Part 1

Addons, plugins, extensions, or any other name they may be called, these are just different names to a very common concept in the Javascript ecosystem. From the years where all our project starts with jQuery, to now where modern frameworks, libraries, or tools such as Vue, Three.js, and ESLint have some level of support for addons. Even projects that don't explicitly have an interface for addons will often see addons written for it to expand on its functionalities. That last point, expanding functionalities, is exactly the idea we are going to explore here.

Unlike a compiled language such as C, C++, Rust, or Java, Javascript is not served to the end user as binary, it is by design served as text. Also unlike other non-compiled language, Javascript is often served frequently over the internet, instead of being packaged and downloaded by the user once to be run multiple time, such as what you might do with any other applications on your computer. This puts Javascript in a position where it needs to do as much as it needed with as little code as necessary: the more code it has, the slower Javascript gets downloaded, the slower the experience it is for the end user.

One method to mitigate this need for small download size is minification and compression. We won't explore that here. The other method that we alluded to is to be as minimal as necessary, to do as much as is necessary and not more. However, it is often a challenge to know what is "necessary". Each library will need to determine that for themselves and we will explore this further in the future, but for now let's take this as a tautology and look at what it means in practice. Once a library decided the necessary functionalities it needs, it gets implemented and people can use it, great! However, there could very well be use cases where people needed a bit more functionalities that are not included, and the library doesn't want to include for various reasons. We don't want our libraries to be able to do everything possible when we are only using a small subset of its capabilities, otherwise our libraries will be excessively bloated. In these kind of cases, it would make sense to create an addon/plugin/extension library that expands on the functionalities of the library.

Depending on the design and purpose of the library, there are several different techniques when it comes to expanding functionalities, none are better than others necessarily, and some libraries are only meant to be expanded one way while others were never meant to be expanded at all. This is to say you should see the following as different options or approaches rather than any kind of value judgement on the techniques themselves.

Note: I will be using the terms "addon", "plugin", and "extension" interchangeably here depending on context and which term individual library uses. They all mean the same thing in our context.

Direct extension

Javascript for better or for worse (depending on who you ask) is a pretty flexible language, especially when it comes to its type system. Javascript is a relatively rare object oriented language that allows the modification of a class definition at runtime, ie. it is able to modify the template in which each object is created from in the middle of execution, this is not something that stricter languages will ever allow. Technically, in Javascript we are not modifying the class definition, for the class syntax that you may have come across in Javascript is mostly syntactic sugar (syntax abstraction) of the real underlying object oriented model that Javascript uses which is prototype based object oriented programming.

We will leave detailed explanation of prototype based object oriented programming for another time, for our purposes here, the key to remember is we can modify classes at any point in time. This means that even if an interface provided by the library has a certain definition, it can still be modified by code that comes later. In practice, we will do this with prototypes and it looks something like the following:

// This class is defined by the library we want to extend
class Rectangle {
  constructor(width, height){
    this.width = width;
    this.height = height;
  }

  area(){
    return this.width * this.height;
  }
}

// In the user code
const r = new Rectangle(10, 20);
console.log(r.area()); // Will print `200`
console.log(r.circumference()); // Will error since there is no `circumference` method
// The following is our "addon" code to extend the `Rectangle` class.
// We will add a `circumference` function to our `Rectangle` class.
Rectangle.prototype.circumference = function(){
  return this.width * 2 + this.height * 2;
};

// In the user code
const r = new Rectangle(10, 20);
console.log(r.area()); // Will print `200`
console.log(r.circumference()); // Will print `60` and no error

As you can see in the second block, by attaching a new function property to Rectangle.prototype, we have modified the original Rectangle class to now have a circumference method that we can call. We can attach whatever we need onto the prototype and it will all behave as if it is defined within the class itself, which our objects will then also inherit from.

This is a relatively common way of extending a class and, with libraries that provide classes for users to create object out of, is a common way of extending functionalities. However, attaching functionalities to a prototype directly using the .prototype syntax is relatively uncommon especially in libraries meant to be extended. There are a few reasons for this, one is because library authors often prefer one of the other methods mentioned below for friendlier syntax (not everyone likes prototypes or understand them) or more granular control over extension capabilities. Another reason you don't see this as much is because in libraries that does utilize prototype based extension, they may choose to obscure that fact. A notable example is jQuery.

jQuery's plugin syntax looks like the following (taken from jQuery's learning page):

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

$( "a" ).greenify(); // Makes all the links green.

We can see that a function is assigned to $.fn called greenify but what is this $.fn? If you are familiar with jQuery, you will know that $ is the conventional short-hand variable for the global jQuery object; fn is a property within that jQuery object, you might have guessed by now that fn is actually prototype! If you open up the web console on the jQuery page linked above and type in the following:

$.fn === $.prototype 

You will see that it returns true. Plugins for jQuery are essentially attaching directly to the jQuery prototype so that all future jQuery object inherits that new functionality that the plugin has attached to the underlying class.

Extension through dedicated API

In a way, we can think of the $.fn syntax that jQuery provides as providing an extension through dedicated API, albeit a very shallow one. However, there are use cases where additional utility and functionality with the plugin system is required. This can be because the library does not just provides a class for object extension, the library provides multiple ways of creating an object, and/or the library has more functionalities that can be extended other than simply creating objects such as if it has a runtime (common for frontend frameworks). In these cases, a more prominent API will often be presented to extend the functionalities of the library, usually in the form of a function that accept some argument that defines the behavior of the plugin.

Let's have a look at what this could mean through an example. Day.js is a library designed to make handling time related data a lot easier in Javascript. It is able to parse timestamp, manipulate time data (eg. calculate the date 10 weeks from now), and format timestamp so that you can display date and time in whatever way you want with as much detail as you need. Day.js provides a plugin system that can extend the possible things you can do with a timestamp such as getting UTC time, do timezone conversions, and many more. To extend Day.js with a plugin, you will do something like the following:

// `dayjs` is the global Day.js object that we use to create timestamp object
// We first use ES6 modules to import our UTC plugin
import utc from "dayjs/plugin/utc";

// Next we extend `dayjs`
dayjs.extend(utc);

Now the global dayjs object has additional functionalities provided by the utc plugin. What is actually happening though? What is the utc plugin and what happens with dayjs.extend() function? If we look at the Day.js plugin documentation page, we will have a good idea on what is potentially happening.

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

A Day.js plugin is always defined as a function, this means that dayjs.extend() is actually expecting an argument with a function as a callback into the .extend() method that is then passed its own relevant arguments. These arguments are option (which is the user defined option that is also passed to .extend() if needed), dayjsClass, and dayjsFactory. In the function body, you will notice that dayjsClass is extended through the prototype syntax we have seen above so that is not new. The final argument dayjsFactory is something that is more Day.js specific, without going into all the details, attaching additional functions to dayjsFactory extends the number of ways Day.js objects can be created.

As you can see in this example, Day.js using a dedicated API for extension does not mean it won't be extending prototypes, but rather it has additional capabilities that is not strictly attached to the class itself. For more complex projects, such as Vue.js or more commonly Node.js based frameworks, using a dedicated API is often the best way to extend the functionality of a library.

Independent interface, compatible data structure

This final method of extension is not really an extension at all. The philosophy here is that instead of having users modify the provided interface of a class or other library internals, the library provides and accepts simple or well-defined data/data structures in its public interface, eliminating much of the need to extend it directly.

What this means is that instead of implementing additional functionality as a dependency onto the existing library, the additional functionality is implemented independently and will either accept outputs from the library in a well-defined format or be able to pass inputs to the library in a well-defined format. In a way, this is the most common method in Javascript because every library that does not provide explicit interface or guidance on extension is assumed to work this way, the only thing the library maintainer need to be aware of is to have said well-defined format for input/output.

<script>
// We can use Chart.js with Vue without needing to extend
// Vue with Chart.js
import { useTemplateRef, onMounted } from "vue";

const chartEl = useTemplateRef("myChart");

onMounted(() => {
  const ctx = chartEl.getContext("2d");

  new Chart(ctx, {
    type: 'bar',
    data: {
      labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
      datasets: [{
        label: '# of Votes',
        data: [12, 19, 3, 5, 2, 3],
        borderWidth: 1
      }]
    },
    options: {
      scales: {
        y: {
          beginAtZero: true
        }
      }
    }
  });
})
script>

<template>
   id="app">
     ref="myChart" />
  
template>

The main benefit of this is that plugin authors will no longer be plugin authors, they will be writing libraries of their own instead. Their end users will also no longer need the upstream dependency if they don't need it, fulfilling even more of the original goal of limiting the transfer over internet of unused code and functionalities. The resulting library have more flexibility and compatibility as well. Gone are the days where every other library were written as a jQuery plugin, these days we can just use whichever library solves our problem without needing to include jQuery if we don't need it anymore.

A final word on extension methods

These above are not necessarily extensive when it comes to all possible ways to extend a library and definitely not extensive when it comes to possible syntax (since that is possibly infinite). I believe most if not all extension variations can fit in at least one of the above category but if you see a variation that somehow don't fit into any of the above category, do let me know, I would love to have a closer look at it.

Now that we have an overview of these different techniques, let's have a look at them in the context of p5.js addon libraries.

Part 2

p5.js as a library has many aspects that a user may want to extend. These can be providing additional drawing functions, communicating with other services, or provide glue code with other libraries, amongst infinite possibilities. The two major direct expansions are providing new functions that can be called in global mode & instance mode semantically, and running hooks (ie. custom code) at certain point in the runtime such as before/after setup() runs and before/after every draw() call. Since we have looked at possible extension strategies above, let's see how they apply in the context of p5.js, starting with p5.js 1.x then we'll look at how p5.js 2.0 changes some aspects of this and why.

p5.js 1.x uses a combination of all three strategies above when it comes to how addons can be authored. Starting from the least complex to explain, the last strategy (Independent interface, compatible data structure) as we know, relies on passing data directly out of or into p5.js. As long as your library can generate parameters accepted by p5.js functions, it will work out of the box. Some examples include passing CSS color string as argument into any function that accepts colors, and using random noise generated by the noise() function in your own addon for smooth randomness.

Next, if you wish for your addon functions to be available globally in global mode, you will need to attach it to the p5.prototype object as per the first method we looked at above.

p5.prototype.polygon = function(points){
  // Draw polygon
};

Most addon libraries that want to extend p5.js will likely work this way. However, in some circumstances, addon libraries may need to also have a bit more control over p5.js, specifically the p5.js runtime. For example, instead of passively providing functions that the user can call, an addon library might want to instead reset the background color at the start of every frame automatically or set up some initial values before setup is called. In these kind of cases, p5.js provides lifecycle hooks that an addon library can hook a function onto. What lifecycle hooks means is that they are specially registered functions that will be run at specific points in the runtime (ie. from initializing the p5 object to running setup() to removing the sketch altogether, if remove() was ever called). To register these special functions onto the relevant hook, we need to use the second extension method, Extension through dedicated API, here.

// This does not need to be a function attached to
// the p5 prototype if it does not require `this`
// and it does not need to be available globally.
p5.prototype.setDefaultBackground = function(){
  // Set background to be p5 pink by default
  this.background("#ed225d");
};
p5.prototype.registerMethod("pre", p5.prototype.setDefaultBackground);

There are a number of hooks available but we will not elaborate on them here as we are interested in how it works in p5.js 2.0 instead. In p5.js 2.0, the requirements and principles stay the same: we still need to be able to attach functions that can be used in global mode and instance mode and we still need a way for library authors to hook into specific lifecycle events. To simplify the addon library authoring workflow, p5.js 2.0 now has a unified dedicated extension API that support both use cases (we are leaving "Independent interface, compatible data structure" out here since the whole point is to not have direct extension in that case). p5.js 2.0 now provides a new static function p5.registerAddon() that takes in one argument which will be the definition of your addon library. This argument is in the form of a function:

const myAddon = function(p5, fn, lifecycles){
};

p5.registerAddon(myAddon);

As you can see above the function that comprise of your addon library has three parameters p5, fn, and lifecycles, let's go through each one at a time.

p5

This is the global p5 object, ie. the p5 constructor. You can use it to attach or access static properties/methods if you need to. For example if you want to manually create a p5.Vector in your library:

const myAddon = function(p5, fn, lifecycles){
  const privateVec = new p5.Vector(0, 1, 0);
};

fn

If you remember fn being used by jQuery, you may have guessed what fn is. If you guessed fn is just an alias for p5.prototype, you will be right! We certainly can access p5.prototype through the p5 parameter in the first position, however fn provides a slight layer of abstraction to the underlying prototype system of Javascript for the library author, it is still functionally the same though.

const myAddon = function(p5, fn, lifecycles){
  fn.polygon = function(points){
    // Draw polygon
  };
};

lifecycles

This is likely the most significant difference and a departure from p5.js 1.x. Instead of having an API to individually register lifecycle hooks, the lifecycles parameter here by default points to an empty object {}. We register lifecycle hooks by assigning relevant keys into the lifecycle hook.

const myAddon = function(p5, fn, lifecycles){
  lifecycles.postsetup = function(){
    // Do some actions after setup finishes.
  };  

  lifecycles.predraw = function(){
    // Set background to be p5 pink by default
    // at the start of each draw call.
    this.background("#ed225d");
  }
};

The available hooks are:

  • presetup - Runs once before setup() runs
  • postsetup - Runs once after setup() runs
  • predraw - Runs once before every draw() runs
  • postdraw - Runs once after every draw() runs
  • remove - Runs once when remove() is called

All functions attached to the lifecycles object have access to the current p5 sketch instance through this (provided they are not defined with arrow functions), and they are all called with the await keyword so they can be async function as well.

Put altogether, we have something like the following:

// This addon sets two default background color for the sketch
// that the user can toggle between by calling the 
// `toggleBackgroundColor` function in global or instance mode.
const myAddon = function(p5, fn, lifecycles){
  const color1 = new p5.Color("floralwhite");
  const color2 = new p5.Color("midnightblue");
  let bgColor = color1;

  fn.toggleBackgroundColor = function(){
    if(bgColor === color1){
      bgColor = color2;
    }else{
      bgColor = color1;
    }
  };

  lifecycles.predraw = function(){
    this.background(bgColor);
  };
};

This syntax should have enough flexibility to cover all uses cases for extension and even if not, there will be room for easy expansion without causing problems with future compatibilities.

Part 3

This part is for those who already has an existing addon library written for p5.js 1.x and want to make it compatible with p5.js 2.0. If you are just here to learn about addon libraries and don't already have a p5.js 1.x addon library, you can skip this section.

The good news is, if your library don't extend p5.js directly it will most likely just work as we have seen above. If your library extends p5.js through attaching to p5.prototype, it will likely just work, because as we've seen, the new addon syntax also provides fn as an alias to p5.prototype and we attach methods there still.

The bad news is, if your library uses the 1.x lifecycle hook register method p5.prototype.registerMethod, you will need to do some conversion. If your addon uses p5.registerPreloadMethod, _incrementPreload, or _decrementPreload, you will also need to update your addon, for this case, please refer to the article on asynchronous p5.js 2.0.

To convert lifecycle hooks, do the following:

  1. Take note of the relevant lifecycle hooks you are using and how they map onto the new names:
    • init -> You no longer need a lifecycle hook for this, just put the relevant code in the addon function body or the presetup hook.
    • beforePreload -> presetup
    • afterPreload -> presetup/postsetup
    • beforeSetup -> presetup
    • afterSetup -> postsetup
    • pre -> predraw
    • post -> postdraw
    • remove -> remove
  2. Using the new addon syntax, attach the relevant function as members of the lifecycles parameter.

    const myAddon = function(p5, fn, lifecycles){
      lifecycles.presetup = function(){
        // Your old `beforeSetup` code
      };
    };
    
  3. Note that since the lifecycle hook functions will have this resolved to the p5 sketch instance already, you no longer need to attach functions to the prototype to access this like we were doing before in the setDefaultBackground example above.

If you are using the new addon syntax, the final thing you will need to do is to call p5.registerAddon() with your defined addon function.

p5.registerAddon(myAddon);

Transition

You may have noted that if your addon library converts to use the new addon library syntax, it will no longer work with p5.js 1.x, since p5.js 1.x does not provide the p5.registerAddon() static function, amongst other reasons. There are a few possible options you can take:

  • Have two different versions of your library, one with p5.js 1.x compatibility and another with p5.js 2.0 compatibility.
  • Release a new version of your library with p5.js 2.0 compatibility. If your user needs p5.js 1.x compatibility, they can use the last released version of your library that was still compatible.
  • Support both syntax at the same time. This will be more complicated to implement and may not be possible in all cases. The general idea will be something like the following:

    // We will attach directly to the `p5` prototype
    p5.prototype.setDefaultBackground = function(){
      // Do some action
    };
    
    // For lifecycles, we check if `p5.registerAddon` is present
    // or not to determine which version to use.
    if(p5.registerAddon){
      // We are working with p5.js 2.0
      const addon = function(p5, fn, lifecycles){
        lifecycles.presetup = function() { /* ... */ }
      };
    
      p5.registerAddon(addon);
    }else{
      // We are working with p5.js 1.x
      p5.prototype.registerMethod("beforeSetup", function() { /* ... */ });
    }
    

If you are unsure how to do the conversion, especially in the specific context of your addon library, please reach out to the p5.js team by opening an issue on the p5.js library repo, directly via email, or other official channel. The p5.js team is currently, at the time of writing, going through the full list of community contributed libraries to individually assess the compatibility of the libraries and identify any action needed. You may be contacted on your library's repo about any action needed. However, feel free to attempt some tests yourself with p5.js 2.0, it may already work without needing any changes to your code.