This is a RFC document for a proposal of p5.js 2.0.
- Build and test system
- Refactors
- Modular
- Libraries
- Renderers
- Async
- Algorithm Changes
- Reference
- Typography
- Misc
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:
- Update p5.js to use more modern JavaScript conventions, both in terms of its internal implementation and the interface it exposes to the user.
- Enable the use of p5.js through ES modules semantically.
- Keeping backwards compatibility with current 1.x version of p5.js as much as reasonable.
A few non-goals of p5.js 2.0 are:
- Do everything possible: p5.js will have a powerful and easy to use addon library system and so should not include every feature possible.
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.
- Rollup
- Significantly improves the build speed of the library with the core library taking just a few seconds to build.
- Have more semantic support of ESM.
- Simpler config and overall a more modern set of tools.
- Vite
- Also Vite has a library mode and uses Rollup under the hood for production build, it lacks the flexibility to support complex library build where multiple inputs need to correspond to multiple outputs.
- Provides a very good development server that can be used to speed up library development. An
index.htmlfile is added to the repo for using this development server.
- Vitest
- Uses the same idea behind Vite's development server to run tests which speeds up tests running significantly.
- Supports headless browsers, parallel tests running, granular test running in watch mode, and more.
- While many tests already works, the overall tests will need to have a pass through to correct everything, update to use latest ESM syntax library will be using, and integrate any new test methods where necessary.
- All tests will run in headless browsers and not test should be run in Node.js as it is not the intended environment for p5.js to run in.
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:
- 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);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.Rendererclass which bothp5.Renderer2Dandp5.RendererGLclasses inherit from will now act more like an abstract class that it is meant to be.- The
p5.Rendererclass 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 theellipse()method but the WebGL renderer will also implement asphere()method that 2D renderers don't need). - The
p5.Rendererclass should never be instantiated directly.
- The
- The core will not have implicit knowledge of what renderers are available. Previously the two modes supported (
P2DandWEBGL) are harded coded into functions likecreateCanvas()making creating a new rendering mode difficult without also modifying core functionalities.- A list of renderers should be kept under the
p5.renderersobject with value being the class object of the renderer (that inherits fromp5.Rendererclass).
- A list of renderers should be kept under the
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:
- Bundling both 2D and WebGL renderers, bundling also any new renderers if they are included in the library directly.
- Bundle only 2D renderer. Log a warning message if the sketch author tries to create a WebGL canvas without loading in the WebGL renderer as an addon.