Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 169 additions & 45 deletions src/core/loading.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,74 +4,198 @@
* @private
*
* Handles the logic for creating a loading indicator.
* Currently, the loading indicator is basic and can be extended in the future.
*/

/**
* Creates a loading indicator when the sketch's setup() function is running.
* It is called and removed automatically using the presetup and postsetup lifecycles hooks.
* It is called and removed automatically using the presetup and postsetup lifecycle hooks.
* Registers loading indicator decorators for createCanvas(), resizeCanvas(), and noCanvas()
* to manage the loading indicator overlay.
*
* @param {*} p5 The p5 constructor
* @param {*} fn The p5 prototype object
* @param {*} lifecycles Lifecycle hooks for the sketch
*/
export default function loading(p5, fn, lifecycles) {
p5.registerDecorator('p5.prototype.createCanvas', _handleLoadingIndicator(true));
p5.registerDecorator('p5.prototype.resizeCanvas', _handleLoadingIndicator(true));
p5.registerDecorator('p5.prototype.noCanvas', _handleLoadingIndicator(false));

lifecycles.presetup = function () {
if (typeof window === 'undefined' || this._loadingIndicator) {
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;
};

lifecycles.postsetup = function () {
if (this._loadingIndicator) {
this._loadingIndicator.remove();
this._loadingIndicator = null;
}
this._isSketchLoading = false;
_removeLoadingOverlay(this);
};
}

/**
* Creates and stylizes the loading indicator.
* As a helper function, it can be extensible and modified in future versions.
* Creates the loading canvas to directly overlay the sketch canvas
* and starts the spinning logo animation loop.
*
* @private
* @param {HTMLElement} container The HTML element to append the indicator to
* @returns {HTMLElement} The loading indicator div element
* @param {p5} pInst The p5 instance.
*/
function createLoadingIndicator(container) {
if (!document.getElementById('p5-loading-style')) {
const loadingStyle = document.createElement('style');
loadingStyle.id = 'p5-loading-style';
loadingStyle.textContent =
'@keyframes p5-loading-spin { to { transform: rotate(360deg); } }';
document.head.appendChild(loadingStyle);
function _createLoadingOverlay(pInst) {
const actualCanvas = pInst.canvas?.elt || pInst.canvas;
if (!actualCanvas) return;

let overlay = pInst._loadingOverlay;

// If overlay doesn't exist yet, create it and animate it
if (!overlay) {
overlay = document.createElement('canvas');
overlay.id = `${actualCanvas.id || 'defaultCanvas0'}_loadingOverlay`;
pInst._loadingOverlay = overlay;

const ctx = overlay.getContext('2d');
let frameCount = 0;

const animate = () => {
if (!pInst._isSketchLoading) return;

ctx.clearRect(0, 0, overlay.width, overlay.height);
_drawLoadingIndicator(
ctx,
overlay.width / 2,
overlay.height / 2,
frameCount++
);

pInst._loadingOverlayFrame = requestAnimationFrame(animate);
};

animate();
}

const indicator = document.createElement('div');
indicator.className = 'loading-indicator';
indicator.style.cssText = `
position: fixed;
inset: 0;
margin: auto;
width: 30px;
height: 30px;
border-radius: 50%;

border: 3px solid rgba(0, 0, 0, 0.1);
border-top-color: rgba(0, 0, 0, 0.8);
animation: p5-loading-spin 1s linear infinite;
z-index: 9999;
`;

container.appendChild(indicator);
return indicator;
// Positions the loading indicator to overlay the sketch canvas
_positionCanvas(overlay, actualCanvas);

if (overlay.parentNode !== actualCanvas.parentNode) {
actualCanvas.parentNode.insertBefore(overlay, actualCanvas.nextSibling);
}
}

/**
* Matches the size and position of the loading canvas to the user's sketch canvas.
*
* @private
* @param {HTMLCanvasElement} loadingCanvas The overlay canvas element.
* @param {HTMLCanvasElement} actualCanvas The sketch canvas element.
*/
function _positionCanvas(loadingCanvas, actualCanvas) {
loadingCanvas.width = actualCanvas.width;
loadingCanvas.height = actualCanvas.height;

const width = actualCanvas.style.width || `${actualCanvas.offsetWidth || actualCanvas.width}px`;
const height = actualCanvas.style.height || `${actualCanvas.offsetHeight || actualCanvas.height}px`;

Object.assign(loadingCanvas.style, {
width,
height,
position: 'absolute',
top: `${actualCanvas.offsetTop}px`,
left: `${actualCanvas.offsetLeft}px`,
margin: '0',
padding: '0',
pointerEvents: 'none',
zIndex: '9999'
});
}

/**
* Stops the loading indicator animation and removes the overlay canvas from the DO
*
* @private
* @param {p5} pInst The p5 instance.
*/
function _removeLoadingOverlay(pInst) {
if (pInst._loadingOverlay) {
cancelAnimationFrame(pInst._loadingOverlayFrame);
pInst._loadingOverlay.remove();
pInst._loadingOverlay = null;
}
}

/**
* Draws a canvas-based animated loading indicator.
* The loading indicator is a spinning p5 logo.
*
* Credits to Raphaël de Courville for creating the p5 logo sketch
*
* @private
* @param {CanvasRenderingContext2D} ctx The 2D canvas context to draw on.
* @param {Number} x The x-coordinate for the logo center.
* @param {Number} y The y-coordinate for the logo center.
* @param {Number} t The frame count used to calculate rotation.
*/
function _drawLoadingIndicator(ctx, x, y, t) {
let rotationSpeed = 3.25;
let indicatorSize = 1.5;

ctx.save();
ctx.translate(x, y);
ctx.scale(indicatorSize, indicatorSize);

ctx.rotate((t * rotationSpeed * Math.PI) / 180);
ctx.translate(-14, -14);

ctx.fillStyle = '#ED225D';
ctx.beginPath();

ctx.moveTo(16.909, 10.259);
ctx.lineTo(25.442, 7.683);
ctx.lineTo(27.118, 12.839);
ctx.lineTo(18.62, 15.738);
ctx.lineTo(23.895, 23.218);
ctx.lineTo(19.448, 26.443);
ctx.lineTo(13.895, 19.095);
ctx.lineTo(8.487, 26.25);
ctx.lineTo(4.169, 22.961);
ctx.lineTo(9.444, 15.738);
ctx.lineTo(0.88, 12.647);
ctx.lineTo(2.558, 7.487);
ctx.lineTo(11.156, 10.258);
ctx.lineTo(11.156, 1.364);
ctx.lineTo(16.91, 1.364);

ctx.closePath();
ctx.fill();
ctx.restore();
}

/**
* Intercepts canvas methods to create, update, or remove the loading indicator
*
* @private
* @internal
*
* @param {Boolean} isLoading True to show the loading indicator; false to hide it.
* @return {Function} A decorator function for the target canvas method.
*/
export function _handleLoadingIndicator(isLoading) {
return function (target) {
return function (...args) {
const result = target.call(this, ...args);

// Create loading overlay if canvas is loading
if (isLoading) {
if (this._isSketchLoading) {
_createLoadingOverlay(this);
}
}

// Remove loading overlay if canvas isn't loading
else {
_removeLoadingOverlay(this);
}

return result;
};
};
}