diff --git a/src/core/loading.js b/src/core/loading.js new file mode 100644 index 0000000000..fa200b5e54 --- /dev/null +++ b/src/core/loading.js @@ -0,0 +1,67 @@ +/** + * @module Loading + * @for p5 + * @private + * + * 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; + + const container = this.canvas?.parentElement || document.body; + loadingIndicator = createLoadingIndicator(container); + }; + + lifecycles.postsetup = function() { + loadingIndicator?.remove(); + loadingIndicator = null; + }; +} + +/** + * 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 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; + 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: loading-spin 1s linear infinite; + `; + + container.appendChild(indicator); + return indicator; +} \ No newline at end of file diff --git a/src/core/main.js b/src/core/main.js index 00e536d900..f002b5ced3 100644 --- a/src/core/main.js +++ b/src/core/main.js @@ -638,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); @@ -646,6 +647,7 @@ p5.registerAddon(rendering); p5.registerAddon(renderer); p5.registerAddon(renderer2D); p5.registerAddon(graphics); +p5.registerAddon(loading); export default p5; diff --git a/test/unit/core/loading.js b/test/unit/core/loading.js new file mode 100644 index 0000000000..79da1992d1 --- /dev/null +++ b/test/unit/core/loading.js @@ -0,0 +1,81 @@ +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'); + container.appendChild(canvas); + document.body.appendChild(container); + }); + + afterEach(function() { + lifecycles.postsetup?.(); + + 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 = { + canvas: canvas, + 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() { + lifecycles.presetup.call(p); + + 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 { + lifecycles.postsetup.call(p); + } + })(); + + const loadingIndicator = container.querySelector('.loading-indicator'); + + assert.exists(loadingIndicator); + + loadTest(); + await setupPromise; + + 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]]); + assert.deepEqual(p.circle.mock.calls, [[200, 200, 100], [12, 34, 20]]); + }); +}); \ No newline at end of file