Skip to content
OVEX TECH
Education & E-Learning

Master p5.js 2.0: Transition from 1.x with Async/Await

Master p5.js 2.0: Transition from 1.x with Async/Await

Master p5.js 2.0: Transition from 1.x with Async/Await

p5.js 2.0 introduces significant changes, most notably the adoption of asynchronous programming patterns like async and await for loading data. This transition can seem daunting, but this guide will demystify these new concepts, showing you how to leverage them for smoother and more efficient sketching. We’ll cover how to update your sketches, understand the underlying reasons for these changes, and implement them effectively.

Understanding the Shift to p5.js 2.0

p5.js 2.0 has been released, marking a new era for the library. While the transition to becoming the default version will occur later, it’s beneficial to start adapting to its new patterns. For beginners, many core functionalities remain the same. However, the way data is loaded has fundamentally changed. This guide focuses on the transition from p5 1.x to 2.0, particularly addressing the use of async and await, and how they replace older methods like preload and callbacks.

Why the Change? Synchronous vs. Asynchronous Operations

JavaScript, and by extension p5.js, differentiates between synchronous and asynchronous operations. A synchronous operation completes all its tasks before moving to the next line of code. An asynchronous operation starts a task in the background and continues executing subsequent code without waiting for the background task to finish.

Loading external data, such as images or JSON files, is inherently an asynchronous process. It involves network requests and data retrieval, which can take time. If these operations were strictly synchronous, they would block the entire program, leading to a frozen interface. To prevent this, JavaScript and p5.js handle them asynchronously.

In p5.js 1.x, the preload() function was introduced to manage these asynchronous loads before the main setup() function began. Callbacks were also used to execute code only after an asynchronous operation completed.

p5.js 2.0 embraces modern JavaScript’s async and await keywords to handle asynchronous operations more elegantly. This allows asynchronous code to be written in a style that closely resembles synchronous code, making it more readable and manageable, especially when dealing with sequential data loading.

How to Switch to p5.js 2.0

Switching your sketch to use p5.js 2.0 is straightforward within the p5.js web editor. Look for a version indicator, often a small chip or badge, in the top right corner of the editor.

  1. Locate the Version Indicator: In the p5.js web editor, find the version information displayed prominently, usually in the upper right section.
  2. Click to Change: Click on this version indicator. A modal window or dropdown will appear, allowing you to select the desired p5.js version.
  3. Select p5.js 2.0: Choose the latest available 2.0 release from the list. The editor will then update the sketch to use this version.

Checking the Version in index.html

The version of p5.js being used by your sketch is specified in the index.html file, typically within a script tag that loads the p5.js library from a Content Delivery Network (CDN). You can manually change the URL in this tag to point to a different version of the p5.js library.

Understanding Promises, async, and await

In p5.js 2.0, functions that load data (like loadImage(), loadJSON(), etc.) no longer directly return the loaded data. Instead, they return a Promise. A Promise is an object representing the eventual completion (or failure) of an asynchronous operation and its resulting value.

A Promise can be in one of three states:

  • Pending: The initial state; the operation has not yet completed.
  • Fulfilled: The operation completed successfully, and the Promise has a resulting value.
  • Rejected: The operation failed, and the Promise has a reason for the failure.

The await keyword is used within an async function to pause the execution of that function until a Promise is settled (either fulfilled or rejected). If the Promise is fulfilled, await returns the resulting value. If it’s rejected, it throws an error.

To use the await keyword inside a function, that function must be declared with the async keyword. This tells JavaScript that the function will contain asynchronous operations.

Implementing async and await for Data Loading

Scenario 1: Loading Data Directly within setup()

Previously, you might have used preload() to ensure data was loaded before setup() began. With async/await, you can load data directly within setup(), making your code more sequential and readable.

  1. Declare setup() as async: Add the async keyword before the setup function declaration: async function setup() { ... }.
  2. Use await for Loading Functions: When calling a data loading function (e.g., loadImage()), prepend it with the await keyword: let myImage = await loadImage('path/to/image.jpg');.
  3. Remove preload(): You no longer need a separate preload() function for these operations.

Example:

let myImage;

async function setup() {
  createCanvas(400, 400);
  myImage = await loadImage('my_image.png'); // await pauses here until image is loaded
  // Code here will only run after the image is loaded
}

function draw() {
  if (myImage) {
    image(myImage, 0, 0);
  }
}

Common Pitfalls and How to Avoid Them

  • Forgetting async: If you use await inside a function, you MUST declare that function as async. Forgetting this will result in an error like “await is only valid in async functions“.
  • Forgetting await: If you forget await before a loading function, the code will continue to the next line immediately, and the variable will hold a pending Promise instead of the loaded data. This often leads to errors when trying to use the data (e.g., trying to draw an undefined image). p5.js 2.0 provides helpful error messages for this scenario.

Debugging Tip: If you encounter issues, use console.log() to inspect the variable. If it logs a “Promise { }”, you likely forgot the await keyword.

Scenario 2: Allowing draw() to Start Immediately (Non-blocking Load)

Sometimes, you might want the sketch to start drawing immediately, perhaps displaying a loading indicator, while assets load in the background. This is achieved by moving the asynchronous loading operation into a separate async function that is called from setup() without await.

  1. Create a separate async function: Define a new function (e.g., loadAssets()) and mark it as async. Place your await calls for loading data inside this function.
  2. Call the function in setup(): In your setup() function, call this new asynchronous function without the await keyword. This starts the loading process but allows setup() to complete and draw() to begin immediately.
  3. Check for loaded assets in draw(): In your draw() function, include a check (e.g., an if statement) to see if the assets have finished loading before attempting to use them.

Example:

let myImage;

async function loadAssets() {
  myImage = await loadImage('my_image.png');
  // More await calls for other assets can go here
}

function setup() {
  createCanvas(400, 400);
  loadAssets(); // Start loading assets, but don't wait
}

function draw() {
  background(220);
  if (myImage) {
    image(myImage, 0, 0);
  } else {
    // Optional: Display a loading indicator
    text('Loading...', width / 2, height / 2);
  }
}

Key Distinction: Calling loadAssets() without await in setup() ensures that setup() finishes quickly, allowing draw() to begin. If you were to use await loadAssets(); in setup(), it would behave like Scenario 1, and draw() would only start after all assets are loaded.

Handling Sequential Loading with async/await

One of the most significant advantages of async/await is simplifying sequential asynchronous operations, often referred to as avoiding “callback hell.” In p5.js 1.x, loading data that depended on previously loaded data often involved nested callbacks, making the code complex and hard to follow.

With async/await, you can load data step-by-step within a single async function.

Example: Loading JSON, then an Image based on JSON data

let dogImage;

async function setup() {
  createCanvas(400, 400);
  
  try {
    // 1. Load JSON data
    let jsonData = await loadJSON('https://dog.ceo/api/breeds/image/random');
    
    // 2. Use data from JSON to load the image
    dogImage = await loadImage(jsonData.message);
    
    console.log('Image loaded successfully!');
  } catch (error) {
    console.error('Failed to load assets:', error);
    // Handle errors, e.g., display an error message
  }
}

function draw() {
  if (dogImage) {
    image(dogImage, 0, 0, width, height);
  }
}

In this example, await loadJSON(...) ensures the JSON is fetched before await loadImage(...) is called with the URL obtained from the JSON. This is significantly cleaner than managing multiple callbacks.

Compatibility Add-on Library

For existing projects that you wish to run with p5.js 2.0 without immediately refactoring all data loading code, p5.js offers a compatibility add-on library.

  1. Enable the Add-on: In the p5.js web editor, navigate to the version selection modal.
  2. Turn on Compatibility: Look for an option like “p5 1.x compatibility add-on library” and switch it to “On.”

This add-on allows older loading methods (like preload() and callbacks) to function within a p5.js 2.0 environment, providing a smoother transition path.

Conclusion

The transition to p5.js 2.0, particularly the adoption of async and await, streamlines asynchronous operations, making code more readable and manageable. By understanding Promises and how to use these keywords, you can effectively load data, handle sequential operations, and build more robust p5.js sketches. While the compatibility library offers a safety net, embracing async/await is key to leveraging the full power of p5.js 2.0.


Source: What's new in p5.js 2! (YouTube)

Leave a Reply

Your email address will not be published. Required fields are marked *

Written by

John Digweed

1,278 articles

Life-long learner.