Skip to content
Open
Show file tree
Hide file tree
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
67 changes: 67 additions & 0 deletions src/core/loading.js
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 2 additions & 0 deletions src/core/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -646,6 +647,7 @@ p5.registerAddon(rendering);
p5.registerAddon(renderer);
p5.registerAddon(renderer2D);
p5.registerAddon(graphics);
p5.registerAddon(loading);

export default p5;

Expand Down
81 changes: 81 additions & 0 deletions test/unit/core/loading.js
Original file line number Diff line number Diff line change
@@ -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]]);
});
});
Loading