From 530581fc480c7b1206d3d2e041a637dcb00fcf66 Mon Sep 17 00:00:00 2001 From: Johnny Huynh Date: Mon, 20 Jul 2026 12:27:11 -0700 Subject: [PATCH 1/5] Implemented a loading indicator that will appear when the canvas is being set up. --- src/core/loading.js | 31 +++++++++++++++++++++++++++++++ src/core/main.js | 12 +++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 src/core/loading.js diff --git a/src/core/loading.js b/src/core/loading.js new file mode 100644 index 0000000000..4a3ee81b45 --- /dev/null +++ b/src/core/loading.js @@ -0,0 +1,31 @@ +/** + * Creates a loading indicator + */ + +let loadingIndicator = null; + +export function showLoadingIndicator(canvas) { + if (!canvas || loadingIndicator) { + return; + } + + loadingIndicator = document.createElement('div'); + loadingIndicator.textContent = 'Loading...'; + + loadingIndicator.style.cssText = ` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: grid; + place-items: center; + `; + + canvas.parentElement?.appendChild(loadingIndicator); +} + +export function hideLoadingIndicator() { + loadingIndicator?.remove(); + loadingIndicator = null; +} \ No newline at end of file diff --git a/src/core/main.js b/src/core/main.js index 00e536d900..2fa3959538 100644 --- a/src/core/main.js +++ b/src/core/main.js @@ -243,7 +243,17 @@ class p5 { const context = this._isGlobal ? window : this; if (typeof context.setup === 'function') { - await context.setup(); + if(typeof window !== 'undefined' && this.canvas) { + showLoadingIndicator(this.canvas); + } + try { + await context.setup(); + } + finally { + if(typeof window !== 'undefined') { + hideLoadingIndicator(); + } + } } if (this.hitCriticalError) return; From 4e45ca154925240e599e896693814b535517a245 Mon Sep 17 00:00:00 2001 From: Johnny Huynh Date: Mon, 20 Jul 2026 13:25:33 -0700 Subject: [PATCH 2/5] Added inline references for loading.js --- src/core/loading.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/core/loading.js b/src/core/loading.js index 4a3ee81b45..0b9d885b05 100644 --- a/src/core/loading.js +++ b/src/core/loading.js @@ -1,9 +1,21 @@ /** - * Creates a loading indicator + * @module Loading + * @for p5 + * @private + * + * Creates a loading indicator when the sketch's setup() function is running. + * Currently, the loading indicator is basic and can be extended in the future. */ let loadingIndicator = null; +/** + * Creates a simple loading indicator at the center of the webpage. + * + * @method showLoadingIndicator + * @param {HTMLElement} canvas The canvas element of the webpage. + * @private + */ export function showLoadingIndicator(canvas) { if (!canvas || loadingIndicator) { return; @@ -25,6 +37,12 @@ export function showLoadingIndicator(canvas) { canvas.parentElement?.appendChild(loadingIndicator); } +/** + * Removes the loading indicator element from the webpage DOM. + * + * @method hideLoadingIndicator + * @private + */ export function hideLoadingIndicator() { loadingIndicator?.remove(); loadingIndicator = null; From a14c933ec9872c2f6f9474c7b39e9789d070be7a Mon Sep 17 00:00:00 2001 From: Johnny Huynh Date: Mon, 20 Jul 2026 14:44:38 -0700 Subject: [PATCH 3/5] Added a unit test for testing the loading indicator. --- src/core/main.js | 1 + test/unit/core/loading.js | 78 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 test/unit/core/loading.js diff --git a/src/core/main.js b/src/core/main.js index 2fa3959538..12eb8d854e 100644 --- a/src/core/main.js +++ b/src/core/main.js @@ -5,6 +5,7 @@ */ import * as constants from './constants'; +import { showLoadingIndicator, hideLoadingIndicator } from './loading'; /** * This is the p5 instance constructor. diff --git a/test/unit/core/loading.js b/test/unit/core/loading.js new file mode 100644 index 0000000000..7f7aaaff6d --- /dev/null +++ b/test/unit/core/loading.js @@ -0,0 +1,78 @@ +import { vi } from 'vitest'; +import { showLoadingIndicator, hideLoadingIndicator } from '../../../src/core/loading.js'; + +suite('Loading indicator', function() { + let container; + let canvas; + + beforeEach(function() { + container = document.createElement('div'); + canvas = document.createElement('canvas'); + container.appendChild(canvas); + document.body.appendChild(container); + }); + + afterEach(function() { + hideLoadingIndicator(); + + if (container) { + container.remove(); + container = null; + canvas = null; + } + }); + + test('shows a loading indicator while async setup waits for load()', async function() { + let loadTest; + + const p = { + createCanvas: vi.fn(), + background: vi.fn(), + fill: vi.fn(), + circle: vi.fn(), + width: 400, + height: 400, + mouseX: 12, + mouseY: 34 + }; + + const load = async delay => { + await new Promise(resolve => { + loadTest = resolve; + }); + }; + + const setupPromise = (async function setup() { + showLoadingIndicator(canvas); + + try { + p.createCanvas(400, 400); + + await load(7000); + + p.background('#EB5580'); + p.fill(255); + p.circle(p.width / 2, p.height / 2, 100); + + p.circle(p.mouseX, p.mouseY, 20); + } + finally { + hideLoadingIndicator(); + } + })(); + + const loadingOverlay = container.querySelector('div'); + + assert.exists(loadingOverlay); + assert.equal(loadingOverlay.textContent, 'Loading...'); + + loadTest(); + await setupPromise; + + assert.isNull(container.querySelector('div')); + assert.deepEqual(p.createCanvas.mock.calls, [[400, 400]]); + assert.deepEqual(p.background.mock.calls, [['#EB5580']]); + assert.deepEqual(p.fill.mock.calls, [[255]]); + assert.deepEqual(p.circle.mock.calls, [[200, 200, 100], [12, 34, 20]]); + }); +}); \ No newline at end of file From 59a96c5496d710639e21dcee224ca147edb8f3d9 Mon Sep 17 00:00:00 2001 From: Johnny Huynh Date: Wed, 22 Jul 2026 11:16:05 -0700 Subject: [PATCH 4/5] Decoupled the loading indicators from main.js and replaced the loading text with a basic animation. --- src/core/loading.js | 68 ++++++++++++++++++++++++--------------------- src/core/main.js | 15 ++-------- 2 files changed, 39 insertions(+), 44 deletions(-) diff --git a/src/core/loading.js b/src/core/loading.js index 0b9d885b05..a59147df04 100644 --- a/src/core/loading.js +++ b/src/core/loading.js @@ -9,41 +9,45 @@ let loadingIndicator = null; -/** - * Creates a simple loading indicator at the center of the webpage. - * - * @method showLoadingIndicator - * @param {HTMLElement} canvas The canvas element of the webpage. - * @private - */ -export function showLoadingIndicator(canvas) { - if (!canvas || loadingIndicator) { - return; - } +export default function loading(p5, fn, lifecycles) { + lifecycles.presetup = function() { + if (typeof window === 'undefined' || loadingIndicator) return; + + const container = this.canvas?.parentElement || document.body; + loadingIndicator = createLoadingIndicator(container); + }; + + lifecycles.postsetup = function() { + loadingIndicator?.remove(); + loadingIndicator = null; + }; +} - loadingIndicator = document.createElement('div'); - loadingIndicator.textContent = 'Loading...'; +function createLoadingIndicator(container) { + if (!document.getElementById('loading-style')) { + const style = document.createElement('style'); + style.id = 'loading-style'; + style.textContent = '@keyframes loading-spin { to { transform: translate(-50%, -50%) rotate(360deg);}}'; + document.head.appendChild(style); + } - loadingIndicator.style.cssText = ` + const indicator = document.createElement('div'); + indicator.className = 'loading-indicator'; + indicator.style.cssText = ` position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - display: grid; - place-items: center; - `; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 30px; + height: 30px; + border-radius: 50%; - canvas.parentElement?.appendChild(loadingIndicator); -} + border: 3px solid rgba(0, 0, 0, 0.1); + border-top-color: rgba(0, 0, 0, 0.8); + animation: loading-spin 1s linear infinite; + z-index: 9999; + `; -/** - * Removes the loading indicator element from the webpage DOM. - * - * @method hideLoadingIndicator - * @private - */ -export function hideLoadingIndicator() { - loadingIndicator?.remove(); - loadingIndicator = null; + container.appendChild(indicator); + return indicator; } \ No newline at end of file diff --git a/src/core/main.js b/src/core/main.js index 12eb8d854e..f002b5ced3 100644 --- a/src/core/main.js +++ b/src/core/main.js @@ -5,7 +5,6 @@ */ import * as constants from './constants'; -import { showLoadingIndicator, hideLoadingIndicator } from './loading'; /** * This is the p5 instance constructor. @@ -244,17 +243,7 @@ class p5 { const context = this._isGlobal ? window : this; if (typeof context.setup === 'function') { - if(typeof window !== 'undefined' && this.canvas) { - showLoadingIndicator(this.canvas); - } - try { - await context.setup(); - } - finally { - if(typeof window !== 'undefined') { - hideLoadingIndicator(); - } - } + await context.setup(); } if (this.hitCriticalError) return; @@ -649,6 +638,7 @@ import rendering from './rendering'; import renderer from './p5.Renderer'; import renderer2D from './p5.Renderer2D'; import graphics from './p5.Graphics'; +import loading from './loading'; p5.registerAddon(transform); p5.registerAddon(structure); @@ -657,6 +647,7 @@ p5.registerAddon(rendering); p5.registerAddon(renderer); p5.registerAddon(renderer2D); p5.registerAddon(graphics); +p5.registerAddon(loading); export default p5; From e44fe78a20298ebd1fa8b079bc182f6038a1d3c4 Mon Sep 17 00:00:00 2001 From: Johnny Huynh Date: Wed, 22 Jul 2026 11:33:31 -0700 Subject: [PATCH 5/5] Refactored loading.js and added inline references. Also modified the unit test for the loading indicator to work with the updated loading.js file. --- src/core/loading.js | 32 +++++++++++++++++++++++--------- test/unit/core/loading.js | 21 ++++++++++++--------- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/core/loading.js b/src/core/loading.js index a59147df04..fa200b5e54 100644 --- a/src/core/loading.js +++ b/src/core/loading.js @@ -3,12 +3,20 @@ * @for p5 * @private * - * Creates a loading indicator when the sketch's setup() function is running. + * Handles the logic for creating a loading indicator. * Currently, the loading indicator is basic and can be extended in the future. */ let loadingIndicator = null; +/** + * 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. + * + * @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) { lifecycles.presetup = function() { if (typeof window === 'undefined' || loadingIndicator) return; @@ -23,21 +31,28 @@ export default function loading(p5, fn, lifecycles) { }; } +/** + * Creates and stylizes the loading indicator. + * As a helper function, it can be extensible and modified in future versions. + * + * @private + * @param {HTMLElement} container The HTML element to append the indicator to + * @returns {HTMLElement} The loading indicator div element + */ function createLoadingIndicator(container) { if (!document.getElementById('loading-style')) { - const style = document.createElement('style'); - style.id = 'loading-style'; - style.textContent = '@keyframes loading-spin { to { transform: translate(-50%, -50%) rotate(360deg);}}'; - document.head.appendChild(style); + const loadingStyle = document.createElement('style'); + loadingStyle.id = 'loading-style'; + loadingStyle.textContent = '@keyframes loading-spin { to { transform: rotate(360deg); } }'; + document.head.appendChild(loadingStyle); } const indicator = document.createElement('div'); indicator.className = 'loading-indicator'; indicator.style.cssText = ` position: fixed; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); + inset: 0; + margin: auto; width: 30px; height: 30px; border-radius: 50%; @@ -45,7 +60,6 @@ function createLoadingIndicator(container) { border: 3px solid rgba(0, 0, 0, 0.1); border-top-color: rgba(0, 0, 0, 0.8); animation: loading-spin 1s linear infinite; - z-index: 9999; `; container.appendChild(indicator); diff --git a/test/unit/core/loading.js b/test/unit/core/loading.js index 7f7aaaff6d..79da1992d1 100644 --- a/test/unit/core/loading.js +++ b/test/unit/core/loading.js @@ -1,10 +1,13 @@ -import { vi } from 'vitest'; -import { showLoadingIndicator, hideLoadingIndicator } from '../../../src/core/loading.js'; +import { vi, suite, test, assert } from 'vitest'; +import loading from '../../../src/core/loading.js'; suite('Loading indicator', function() { let container; let canvas; + const lifecycles = {}; + loading(null, null, lifecycles); + beforeEach(function() { container = document.createElement('div'); canvas = document.createElement('canvas'); @@ -13,7 +16,7 @@ suite('Loading indicator', function() { }); afterEach(function() { - hideLoadingIndicator(); + lifecycles.postsetup?.(); if (container) { container.remove(); @@ -26,6 +29,7 @@ suite('Loading indicator', function() { let loadTest; const p = { + canvas: canvas, createCanvas: vi.fn(), background: vi.fn(), fill: vi.fn(), @@ -43,7 +47,7 @@ suite('Loading indicator', function() { }; const setupPromise = (async function setup() { - showLoadingIndicator(canvas); + lifecycles.presetup.call(p); try { p.createCanvas(400, 400); @@ -57,19 +61,18 @@ suite('Loading indicator', function() { p.circle(p.mouseX, p.mouseY, 20); } finally { - hideLoadingIndicator(); + lifecycles.postsetup.call(p); } })(); - const loadingOverlay = container.querySelector('div'); + const loadingIndicator = container.querySelector('.loading-indicator'); - assert.exists(loadingOverlay); - assert.equal(loadingOverlay.textContent, 'Loading...'); + assert.exists(loadingIndicator); loadTest(); await setupPromise; - assert.isNull(container.querySelector('div')); + assert.isNull(container.querySelector('.loading-indicator')); assert.deepEqual(p.createCanvas.mock.calls, [[400, 400]]); assert.deepEqual(p.background.mock.calls, [['#EB5580']]); assert.deepEqual(p.fill.mock.calls, [[255]]);