Skip to content

loading.js uses canvas-based animations and sketch instances have their own indicator - #9119

Open
Jextic wants to merge 13 commits into
processing:mainfrom
Jextic:issue-8922
Open

Jextic wants to merge 13 commits into
processing:mainfrom
Jextic:issue-8922

Conversation

@Jextic

@Jextic Jextic commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Resolves #8922

Changes:

  • Changed the CSS style loading indicator to a canvas-based loading indicator. It now uses a spinning p5 logo instead of a generic spinning circle. Thanks to Raphaël de Courville for the sketch code.
  • Changed _createLoadingIndicator() to _createLoadingOverlay. _createLoadingOverlay creates another canvas (which will contain the loading indicator) right on top of the user's canvas (or default canvas).
  • Helper functions _positionCanvas(), _removeLoadingOverlay(), and drawLoadingIndicator() were added to help center the loading indicator, remove it when it's no longer loading, and use draw the p5 logo using canvas functions.
  • Registered the decorator _handleLoadingIndicator() to intercept the canvas whenever canvas-related functions (createCanvas, resizeCanvas, and noCanvas) are called on.

Screenshots of the change:

Video of instance mode sketches:

instance-mode-loading.mp4

This uses the instance mode sketch provided in the issue comment thread plus a modified version of the issue's first sketch example to show that the loading indicator will resize itself respectively to when the canvas resizes.

PR Checklist

@p5-bot

p5-bot Bot commented Sep 1, 2026 •

Copy link
Copy Markdown

@limzykenneth

Copy link
Copy Markdown
Member

@Jextic The test is failing because of https://github.com/processing/p5.js/blob/main/test/unit/core/loading.js#L9 passing null to the first argument of the loading addon function. You will need to pass in a mock, you can have a look at other test files on how to use the mock import { mockP5, mockP5Prototype } from '../../js/mocks';

@Jextic

Jextic commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

@limzykenneth Thank you. I changed the test files to use mockP5 and mockP5Prototype so the setup should work correctly now. I ran the test and everything passed except for the typography one.

@limzykenneth limzykenneth left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One inline and a couple related things:

  1. Is there a way for the user to opt out of having the default loading animation?
  2. Currently the loading animation will still draw on top of what the user already drawn in setup, is there an easy way to avoid it? eg.
async function setup() {
  createCanvas(400, 400);
  background(200);
  ellipse(width / 2, height / 2, 200);
  await new Promise((resolve) => setTimeout(resolve, 2000));
}

This will draw the loading animation on top of the background and ellipse which may not be ideal. Possibly the capability to disable this would be enough but if it can be done relatively manually it would be great as well.

Comment thread src/core/loading.js Outdated
Comment on lines +25 to +28
if (typeof window === 'undefined') {
return;
}

const canvasParent = this.canvas?.parentElement;
let container = this._userNode || canvasParent || document.body;

if (typeof container === 'string') {
container = document.getElementById(container) || document.body;
}

this._loadingIndicator = createLoadingIndicator(container);
this._isSketchLoading = true;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if this module explicitly expects to be running in the browser environment, rather than checking window here, it can instead be conditionally registered in main.js based on whether window is defined or not, so the decorator will never be registered if they are not needed as well.

@Jextic

Jextic commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

I just added noLoadingIndicator() as a function for sketch users so that they can opt out of the loading indicator.
For example, this sketch would just disable the loading indicator so that the visuals won't conflict with each other:

async function setup() {
  createCanvas(400, 400);
  background(200);
  ellipse(width / 2, height / 2, 200);
  noLoadingIndicator();
  await new Promise((resolve) => setTimeout(resolve, 4000));

  ellipse(width / 4, height / 4, 100);
}

What do you think about changing the opacity of the user's canvas when the loading indicator appears. That would make it so that only the indicator would appear when the sketch is loading. This would ensure that the visuals won't conflict but it also means user won't be able to see the canvas partially load. All the contents of the canvas will only appear once everything has been loaded.

@limzykenneth

Copy link
Copy Markdown
Member

@Jextic I'm thinking if there would be a potential solution that naturally leads to a user being able to define their own loading animation. It does not need to happen now but it also should not block a later implementation of it. For example, instead of a noLoadingIndicator() function, something like a loadingAnimation(callback) function where the callback is a function that defines a custom loading animation and not providing the callback means no loading animation.

Changing the opacity of the user's canvas may be a bit too disruptive if it aims to hide the canvas entirely. Perhaps the default loading animation itself has a block grey background with a low opacity? Depends on how it looks.

Another thing that I just came across is that ideally the animation will also cancel in the event that the promises being awaited in the setup function rejected. Currently it seems like when a promise reject, the loading animation still stay around which can be a bit confusing.

Thanks for your patience and work in getting this done, it is a relatively significant piece of work and we appreciate your time and effort in getting this right.

@Jextic

Jextic commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

Sorry it took some time. I replaced noLoadingIndicator() to loadingAnimation(function) so that it takes in a callback to draw to overwrite the default loading indicator. For example, the follow sketch will make a spinning square loading indicator instead:

function drawLoadingAnimation(ctx, width, height, frame) {
  const centerX = width / 2;
  const centerY = height / 2;
  const squareSize = 30;

  ctx.save();
  ctx.translate(centerX, centerY);
  ctx.rotate(frame * 0.05);
  ctx.fillStyle = '#ED225D';
  ctx.beginPath();
  ctx.rect(-squareSize / 2, -squareSize / 2, squareSize, squareSize);
  ctx.fill();
  ctx.restore();
}

async function setup() {
  loadingAnimation(drawLoadingAnimation);

  createCanvas(400, 400);
  background("#888888");
  ellipse(width / 2, height / 2, 200);
  await new Promise((resolve) => setTimeout(resolve, 4000));
}

It was a bit difficult to try to make it more beginner friendly so for now, loadingAnimation() takes in a function that is created similarly to how the default loading indicator is made. If loadingAnimation() is empty, no loading indicator will be created (works exactly like noLoadingIndicator()).

transparent-loading.mp4

I tested out the semi-transparent overlay on the canvas when the loading indicator is present and it doesn't look too bad on simple sketches. Currently, the loading indicator overlays a canvas-sized rectangle with 50% opacity. Setting it to about 20-30% would make the loading indicator stand out more but then the colors seem pretty diluted.

Do you think the loading indicator should scale according to the canvas size? I think it could scale according to either the minimum width or height (mostly so that it wouldn't squish or stretch). However, it would look inconsistent if there are multiple sketches of different sizes.

Comment thread src/core/loading.js

container.appendChild(indicator);
return indicator;
Object.assign(loadingCanvas.style, {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing we don't handle yet is the case when something else moves around the canvas from this initial position, or has other elements on top of the canvas (e.g. UI elements; we don't want this canvas intercepting their mouse events) which they might not want a loading canvas to go on top of. All of those seem somewhat difficult to handle without it being brittle.

Do we have ways to either opt out of showing a loading indicator (would that be passing a custom animation that does nothing? still creates a second canvas though), or ideally to reuse the same canvas? The latter does seem like it'd address all the issues, but comes with some additional things to handle, namely:

  • it might not be a 2D canvas always, and it might change when createCanvas is called. For the default p5 logo spinner we could check the renderer type and handle both; for custom loaders, if they don't want to do that, they could just make sure to create their canvas first before setting up their custom loader so that the type doesn't change
  • Ideally using p5 methods instead of raw canvas context, as especially for webgl/webgpu that would be pretty complex for users to write. In theory p5 methods would be available right?

Sorry to take a step back and discuss some fairly different approaches; just want to make sure we're preempting issues that'd come up by adding a second canvas to every sketch before moving forward!

@limzykenneth limzykenneth Oct 6, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we perhaps have something a bit equivalent to createCanvas that is used in the drawLoadingAnimation function that if not called will not create a second canvas and can also be used to create a 2D or 3D canvas depending on renderer chosen?

Reusing the same canvas will need more work though at this point to accommodate different renderers given we need to get the API used inside drawLoadingAnimation to behave consistently regardless. Also the problem of how to undo the loading animation once it is completed without erasing what is already on the canvas, perhaps by saving the current pixels of the canvas and writing it back after?

To provide a set of p5 like functions without worrying about it clashing with the global definition, we can potentially use the pattern similar to here: https://github.com/antfu/p5i#usage

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we perhaps have something a bit equivalent to createCanvas that is used in the drawLoadingAnimation function that if not called will not create a second canvas and can also be used to create a 2D or 3D canvas depending on renderer chosen?

You probably want to avoid creating extra 3D canvases if possible since there's a fairly low limit in the number of active contexts the browser supports at once, so I think if you're creating extra canvases, it would make sense to limit them to 2D. If we end up not wanting to have loaders on the same canvas, then we maybe just need a way to say "I don't want any loading animation canvas" separate from "here's my own loading animation"

Also the problem of how to undo the loading animation once it is completed without erasing what is already on the canvas, perhaps by saving the current pixels of the canvas and writing it back after?

I suppose if you're interleaving drawing with loading in setup it's going to be pretty hard to do that. It might mean that we would want to have a way to say "I'm done loading" earlier than the end of setup if we have a loader drawn to the main canvas and you intend to do some drawing that you don't want overdrawn by the loader, so you just see the loader while it's loading and then you just see your content afterwards?

To provide a set of p5 like functions without worrying about it clashing with the global definition, we can potentially use the pattern similar to here: https://github.com/antfu/p5i#usage

Does loading start showing up right before presetup hooks start, or is it earlier? If it's right as the p5 lifecycle starts then would the p5 methods already be available?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if we stick with a 2D context always then it might be more straightforward to still do an additional canvas since we can't reuse the canvas already used for 3D, also the user may not have created a canvas in the first place for us to know which renderer they are going to use.

If that is the case then the undo issue won't be a concern and we can ignore it. The original question of a more dynamic sketch at the start that move its position or covered by other elements feels like there could still be a solution though, I think as long as we can keep the loading canvas as a sibling to the main canvas it should take care of most things, there are some edge cases such as when the user use CSS specifically to target only the main canvas.

The loading do start with presetup so p5 methods might be available already, need to confirm how it behaves though.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sticking with 2D for simplicity is ok if we're going to stick with a separate canvas, but then let's just add an escape hatch to disable this entirely if we do

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we add a flag p5.loadingAnimation which when set to false will skip the loading animation stuff entirely so a canvas won't be created?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[p5.js 2.0+ Bug Report]: Loading indication

3 participants