rfc_p5js_2.md
This is a RFC document for a proposal of p5.js 2.0.
p5.js 1.0.0 was released on February 29, 2020, while it may not seem to be that long ago (a bit less than four years ago at the time of writing) in the very fast moving landscape of JavaScript it is a great amount of time. In this time, the JS landscape has advanced a lot with new paradigms and expectations of what a JS library should be. In the time between the 1.0 release till now, p5.js has not been standing still either, with major progress and focuses on accessibility features, bug fixes, updates & enhancements to existing features, and more, contributed by a great number of contributors consistently over the past several years.
However by being within a semver 1.0 release means that we have to consider full backwards compatibility of the library when reviewing any proposals around bug fixes, feature enhancements, and potentially adding new features. At the same time we have continously put off updating several aspects of the library, including things such as tooling, algorithms, and newer APIs, because of either lack of maturity in the JavaScript ecosystem or the overall scope of changes. With the view that p5.js will be getting a new website and simply just because this is long overdue, we propose this RFC to start the process of overhauling many aspects of p5.js and bring about a 2.0 version release.
This project will continue to adhere to p5.js being an accessible and inclusive creative coding library first and foremost, all the proposals outlined below will continue to aim towards increasing access of p5.js. On a technical level, the larger goals of this project are:
A few non-goals of p5.js 2.0 are:
Some exploratory work has been done and can be explored here with future work to continue from it if feasible. The goal is to complete all necessary work listed and agreed upon below by end of March 2024 with a full release in April 2024. Public issues and a project tracker will be used to build up a clear roadmap towards an April 2024 release.
The first stage of this process is for the whole community of p5.js to discuss and review the proposals below and to put forward their own proposals for p5.js 2.0. Exploratory prototyping can be done at this stage using the above linked exploratory version as a starting point. There may be a few rounds of synchronous calls available for community to discuss proposals in real time, the result of which will continue to be tracked here. This document will be constantly updated at this stage as proposals are hashed out, added, or removed throughout the discussions.
The second stage will focus on implementation work. Proposals should be hashed out and accepted at this point to start implementation, continuing from the exploratory branch as the working branch and using feature branches for implementation and submitting PR back to the working branch.
The third and final stage will be the prerelease process. Several prerelease (Release Candidate, RC) versions will be released in the run up to the final release in April 2024. Implementation work in the second stage should be completed at this point and features will be frozen at this point onward, only bug fixes should be worked on at this stage.
After stage 3, p5.js 2.0 will be released!
During this process, p5.js 1.0 will still continue to be worked on and we will review bug reports, fixing them, and create releases as necessary. However we will not review feature enchancements or new feature requests for p5.js 1.0, these should be filed towards a proposal for p5.js 2.0 instead.
See the below for the full list of current proposals under considerations. There may be overlaps between different proposals, which may be assigned to the same contributor for implementation in the second stage. For the purpose of derisking the use of bundled external dependencies, wherever possible, external dependencies should be avoided with preference to original implementation or inlining implementations of external dependencies, if external dependencies are to be used for practical reasons, they need to be fully vetted in stage 1.
The build system for p5.js will be updated to use Rollup, development server will use Vite, and the test runner will use Vitest.
index.html file is added to the repo for using this development server.In conjuction with refactoring and modularization, the build need to be updated as necessary.
The Vite development server can benefit from a more comprehensive index.html that includes common visual regression cases that contributors can check while they work on the code base.
The majority of pending work will be to update all tests to work with Vitest. The priority being all the existing unit tests. Visual tests may be added after.
The overall codebase will undergo extensive refactoring, with the goal of using more semantic JavaScript syntax (modules, classes, newer APIs). The use of side effect imports in the codebase will also be reduced to a minimum if complete removal is not possible.
The overall public API of p5.js should be checked for consistency to ensure a uniform API is provided to the sketch authors where possible. An example for this is beginShape() by default does not close the path without endShape(CLOSE) being provided whereas beginContour() does. The behavior between the two should be made consistent, either both default to closing the path or both default to not close the path. Breaking changes are allowed for these cases.
Code marked as deprecated in the code base should be removed. Any older behavior if agreed to be kept around for the time being but will be removed in the future should be marked as deprecated with deprecation message printed in the console when used, this should be avoided as much as possible however.
p5.js will have a core that contains only the absolute essential functionalities, while all other code will be separately import-able. Take as example the core only build of p5.js and the math module as a separate module not included in core, two versions of each will be built from the source code: IIFE and ESM. Rollup is setup to create build for both with IIFE being the preferred format for most users using <script> tags and ESM the preferred format for users using their own bundlers. (CommonJS or AMD format will not be supported)
Immediately Invoked Function Expression (IIFE) is a common format libraries meant to be included with regular <script> tag will come in. It prevents excessive global namespace pollution and is also used by p5.js 1.0. Rollup have several output formats and libraries meant to be included with regular <script> tag will either be using iife or umd, the later of which combines IIFE, CommonJS, and RequireJS/AMD module syntax in one. p5.js 2.0 will mainly use Rollup's iife format as IIFE allows for initialization through just side effects while for UMD, an export name must be set which is not compatible with the IIFE usage we want.
<script src="./lib/p5.js"></script>
<script src="./lib/p5.math.js"></script>
<script>
function setup(){
createCanvas(400, 400);
console.log(ceil(2.1));
}
function draw(){
background(200);
circle(200, 200, 100);
}
</script>
In the above example, both p5.js and p5.math.js are built with the iife format. This usage is similar if not identical to the usage of addon libraries currently (deliberately so). The math module here is a bundled module taken from all the source located in src/math folder. Each file in that folder can be independenly built into the iife format with Rollup if desired and the whole module can be included in the final p5.js bundle if desired as well.
// src/math/index.js
import calculation from './calculation.js';
import noise from './noise.js';
import random from './random.js';
import trigonometry from './trigonometry.js';
import math from './math.js';
import vector from './p5.Vector.js';
export default function(p5, fn){
p5.registerAddon(calculation);
p5.registerAddon(noise);
p5.registerAddon(random);
p5.registerAddon(trigonometry);
p5.registerAddon(math);
p5.registerAddon(vector);
}
// src/math/calculation.js (redacted)
function calculation(p5, fn){
fn.abs = Math.abs;
}
export default calculation;
if(typeof p5 !== 'undefined'){
calculation(p5, p5.prototype);
}
// src/app.js (redacted)
// math (include if to be bundled as part of p5.js)
import './math/calculation';
import './math/math';
import './math/noise';
import './math/p5.Vector';
import './math/random';
import './math/trigonometry';
Please see relevant examples in the exploration fork for implementation.
ESM or ES Module is the current standard in JavaScript for working with modular JavaScript code. ESM are now very widely supported with all major browsers natively supporting it, all modern build tools supports or are even built around it, and Node.js have native support for it as well. p5.js 1.0's code is already written with ESM and transpiled into a UMD module. As part of the refactor mentioned in a previos section, the syntax of the internal use of ESM will be updated to match semantic usage. The main goal will be to limit cross dependencies between modules and minimize the use of side effects imports.
import p5 from 'p5';
import math from 'p5/math';
p5.registerAddon(math);
// The same instance mode syntax
const sketch = (p => {
p.setup = () => {
p.createCanvas(400, 400);
console.log(p.ceil(2.1));
};
p.draw = () => {
p.background(200);
p.circle(200, 200, 100);
};
});
new p5(sketch);
The above example assumes the user is using Node.js module resolution and have installed p5 through NPM. However, distributable ESM modules are built and will be published via CDN as well. To use this, the first two lines will instead be:
import p5 from './js/p5.esm.js';
import math from './js/p5.math.esm.js';
Across both formats above, one thing that was not elaborated on is the static method p5.registerAddon. This will be discussed in the Libraries section below.
Existing libraries should have a level of compatibility or require minimal updates to work with p5.js 2.0.
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:
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);
p5.js 1.0 is bundled with two renderers: 2D and WebGL. They corresponds to the HTML Canvas 2d and webgl context respectively. However, there had been requests over the years to add additional renderers such as an SVG renderer or a renderer with scene graph capabilities. As the web evolve, we are also seeing new a possible standard renderer being developed, ie. WebGPU.
As currently implemented, adding a new renderer to p5.js is not an easy task which involves many parts that expects to behave differently depending on whether the current sketch is in 2D or WebGL mode.
createCanvas(400, 400, WEBGL);
The constant value to determine whether a canvas is in 2D or WEBGL mode is also not easily extendable by addon libraries.
With p5.js 2.0, the renderer system is redesigned and tweaked with a few key points.
p5.Renderer class which both p5.Renderer2D and p5.RendererGL classes inherit from will now act more like an abstract class that it is meant to be.
p5.Renderer class will determine a set of basic properties and methods any renderer class inheriting from it should implement, while extra functionalities can still be implemented on top. (eg. all renderers should implement the ellipse() method but the WebGL renderer will also implement a sphere() method that 2D renderers don't need).p5.Renderer class should never be instantiated directly.P2D and WEBGL) are harded coded into functions like createCanvas() making creating a new rendering mode difficult without also modifying core functionalities.
p5.renderers object with value being the class object of the renderer (that inherits from p5.Renderer class).p5.renderers = {
[constants.P2D]: Renderer2D,
[constants.WEBGL]: RendererGL
};
For an addon library to create a new renderer to work with p5, it will need to first create a class that inherits and implements the p5.Renderer abstract class, then register the renderer under the p5.renderers object.
(function(p5){
class MyRenderer extends p5.Renderer {
ellipse(x, y, w, h) {
// ...
}
// ...
}
p5.registerAddon((p5, fn, lifecycles) => {
p5.renderers.myRenderer = MyRenderer;
});
})(p5);
When a sketch author wants to use the addon provided renderer above, they can use the following code when creating a canvas.
function setup(){
createCanvas(400, 400, 'myRenderer');
}
For usage that are more similar to p5.js' own renderers, constants registration can be exposed to addon library authors as well. In this case, it is recommended to make the constant value a Symbol matching behavior in the core library itself.
Part of the motivation for this proposal is to enable a leaner build of the library for users who only use the 2D renderer but not the WebGL renderer and vice versa. If someone uses only the 2D canvas, there is no need for most if not all of the WebGL components to be included.
This creates a question, should p5.js still be bundled with both renderers for distribution? What about new renderers in the future? There are a few options for this:
The third options is probably too extreme and probably should not be considered. Either of the first two options are open for discussions.
Data loading functions will all be updated to be fully async using promises (with async/await) while keeping the callback syntax if necessary. setup() will be awaited in the runtime and if the user define it as async functions, the promisified data loading functions can be awaited in them. With an async setup() function, the draw() loop will only start after the setup() function has resolved.
draw() can be defined as an async function as well, although this is not recommended because of the possible timing conflict it may have with requestAnimationFrame.
The async setup() function eliminates the need to have the preload() function. As such the data loading codebase will be refactored to remove preload() related code, while documentation around data loading should be updated accordingly.
Seeded random number generator (RNG) will use xorshift128+. The current algorithm for RNG used in p5.js is an version of a Linear Congruential Generator (LCG).
The main reasoning for this change is for performance. xorshift128+ performs better than LCG and it is the internal implementation of most of the current major browsers for Math.random(). The random property of xorshift128+ is similar if not better than LCG.
This will be a breaking change as existing seeded RNG will not give the same random number sequence once the algoritm has switched to use xorshift128+. Consideration can be made to create a compatibility addon library that patch seeded RNG to use the old LCG RNG if necessary.
Example implementation of a xorshift128+ backed RNG with compatible API to p5.js can be found here.
noise() function will use Simplex noise instead of the current Perlin noise. Simplex noise has better performance than Perlin noise and no noticeable directional artifacts.
Until January 8, 2022, Simplex noise is protected by a US Patent, making adopting it problematic in certain cases. The patent has since expired, opening the way for its implementation in p5.js. This will be a breaking change as existing seeded noise() output will not give the same output after the algorithm has switched to use Simplex noise. Consideration can be made to create a compatibility addon library that patch seeded noise() to use Perlin noise if necessary.
noise() will also accept higher dimensional input with the new implementation through accepting any number of numerical arguments passed to it (currently only accept either 2 or 3 arguments).
A new color module will be implemented. The requirements of this new color module are:
A new math module will be implemented. The requirements of this new math module are:
The inline reference of p5.js while following a largely compatible syntax with JSDoc, under the hood is actually compiled with YUIDoc into a JSON file that the website can consume to render the actual reference page. While YUIDoc serves its purpose since the first reference documentation of p5.js, it is a tool that is no longer being worked on for over 6 years. In these 6 years, many issues and limitations means that p5.js is tied in some ways to how YUIDoc expects documentation to be authored. Several issues with setting up a development environment has also previously been tied to YUIDoc getting outdated with the overall JS ecosystem.
While YUIDoc has a very similar syntax with JSDoc, there are subtle differences. For p5.js 2.0, we will move inline reference authoring to use the JSDoc syntax and generation/compilation to use Documentation.js.
JSDoc syntax has for the most part settled into being a de facto standard for documentation authoring in the JavaScript ecosystem, in fact the current inline reference of p5.js is actually full compatible with JSDoc, in that it successfully compiles with JSDoc with no errors. This does not mean documentation generated through JSDoc will have the correct structure, only that it is valid syntax.
Depending on the next step and the structure of what the website might expect from the reference data file, there will likely need to be minor changes to the reference throughout the codebase.
For compililng the inline reference into data file that can be used by the website to generate the reference pages, instead of using JSDoc, Documentation.js will be used. The reason to prefer Documentation.js over JSDoc in mainly in terms of build tool ergonomics. JSDoc is designed as a site generator with its ability to generate JSON output more of a debugging functionality instead of an intended feature; Documentation.js however supports generating JSON data files as its primary output.
Documentation.js does not have as active a maintenance history as JSDoc which is a potential downside. However, since it fully supports JSDoc syntax and JSDoc syntax having widespread ubiquity, in the future if need be the transition to another tool (even JSDoc) should not require the same effort as transitioning from YUIDoc to JSDoc since the inline reference will stay the same still.
A benefit fully adopting JSDoc syntax is that it enables direct generation of Typescript type declarations (and type checks if desired) from the JSDoc definitions. This is similar to how the source code of SvelteKit is authored using just JavaScript while keeping type generation and type checks.
This feature however is a nice to have. p5.js will aim to follow typing rules as much as possible but will not impose it as a strict requirement for contributors. Type declaration generation will also not be brought into the repo itself (including type checks) which also means types won't be published officially.
The typography module can benefit from an overall refactor or even rewrite. The aim should be to leverage native browser/CSS capabilities as much as possible and limit the use of external library (ie. opentype.js) to only things not achievable otherwise.
These are a collection of minor modifications that don't necessarily have enough scope to be a full proposal on its own.
p5.Table for more semantic JavaScript data structure.Symbol.loadJSON function.
eval() partly because it is not compatible with Rollup's bundlingThe new architecture design should aim for as much pluggability as possible, enabling additional features to be changed, added, or extended through addon libraries. Some of the possible future features that may be implemented are listed below along with reasons they are and are not targeted for immediate implementation.
p5.Graphics in web worker, potentially offloading frequent drawing of many graphics into multi-threaded like environment.creatElement('canvas') then transferControlToOffscreen() (and other methods) to move a p5.Graphics canvas to the web worker. The offscreen canvas can then be drawn onto the on screen canvas with drawImage().transferToImageBitmap() is too slow to achieve real time performance.The 1.x version of p5.js will continue to receive bug and critical fixes for another 12 months after the release of p5.js 2.0. No new features or feature updates will be accepted to 1.x version of p5.js and new releases will be created only when as necessary. Any existing code or addon libraries should aim to migrate to support p5.js 2.0 where possible.
All the above ideas for p5.js 2.0 would not be possible without all the people who has worked on p5.js over the years, all the contributors who provided so much insight and ideas that greatly inspired this project, the many different open source projects and their contributors that directly or indirectly enabled p5.js to be the project that it is, and many many more.
We welcome you to participate in this process of realizing p5.js 2.0 and to continue contributing to p5.js in the future as well!