Stabilize playback state and cursor feedback
This commit is contained in:
+174
-41
@@ -44,6 +44,7 @@ const ModuleLoader = (function() {
|
||||
let gameLoopModule = null; // Add variable to hold game loop instance
|
||||
let moduleTimings = {}; // Track timing data for modules
|
||||
let finalizationTimer = null;
|
||||
let moduleExitAnimations = new Map();
|
||||
|
||||
/**
|
||||
* Initialize the loader
|
||||
@@ -598,6 +599,7 @@ const ModuleLoader = (function() {
|
||||
// If no overlay exists in the HTML, create a minimal one
|
||||
loadingOverlay = document.createElement('div');
|
||||
loadingOverlay.className = 'loading-overlay';
|
||||
loadingOverlay.style.backgroundColor = '#000';
|
||||
loadingOverlay.style.transition = 'opacity 0.5s ease-out';
|
||||
document.body.appendChild(loadingOverlay);
|
||||
|
||||
@@ -639,6 +641,7 @@ const ModuleLoader = (function() {
|
||||
modulesList = loadingOverlay.querySelector('#modules-list');
|
||||
|
||||
// Ensure transition is set
|
||||
loadingOverlay.style.backgroundColor = '#000';
|
||||
loadingOverlay.style.transition = 'opacity 0.5s ease-out';
|
||||
}
|
||||
}
|
||||
@@ -728,16 +731,7 @@ const ModuleLoader = (function() {
|
||||
// if (areAllModulesComplete()) {
|
||||
// hideLoadingOverlay();
|
||||
// }
|
||||
const moduleItem = document.getElementById(`module-${moduleId}`);
|
||||
if (moduleItem) {
|
||||
// Ensure module-finished class is added with a small delay to avoid race conditions
|
||||
setTimeout(() => {
|
||||
moduleItem.classList.add('module-finished');
|
||||
moduleItem.addEventListener('animationend', () => {
|
||||
moduleItem.remove();
|
||||
}, { once: true });
|
||||
}, 120);
|
||||
}
|
||||
animateModuleItemExit(moduleId);
|
||||
} else if (state === ModuleState.ERROR) {
|
||||
moduleProgress[moduleId] = 100;
|
||||
}
|
||||
@@ -810,42 +804,40 @@ const ModuleLoader = (function() {
|
||||
/**
|
||||
* Finalize the loading process
|
||||
*/
|
||||
function finalizeLoading() {
|
||||
async function finalizeLoading() {
|
||||
console.log('Loading completed. Finalizing...');
|
||||
try {
|
||||
// Display timing data
|
||||
displayModuleTimings();
|
||||
|
||||
completeFinalization();
|
||||
await completeFinalization();
|
||||
} catch (error) {
|
||||
console.error('Error during finalization:', error);
|
||||
// Force hide the overlay even if there was an error
|
||||
hideOverlay();
|
||||
await hideOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete the finalization process
|
||||
*/
|
||||
function completeFinalization() {
|
||||
async function completeFinalization() {
|
||||
isLoadingComplete = true;
|
||||
|
||||
// Call the start method on the game loop module directly
|
||||
// Ensure the game loop module was found during initialization
|
||||
if (gameLoopModule && typeof gameLoopModule.start === 'function') {
|
||||
// Hide the overlay first, then start the game loop
|
||||
hideOverlay(() => {
|
||||
console.log("Loader: Overlay hidden, starting Game Loop.");
|
||||
try {
|
||||
gameLoopModule.start();
|
||||
} catch (error) {
|
||||
console.error("Error starting Game Loop:", error);
|
||||
}
|
||||
});
|
||||
await hideOverlay();
|
||||
console.log("Loader: Overlay hidden, starting Game Loop.");
|
||||
try {
|
||||
gameLoopModule.start();
|
||||
} catch (error) {
|
||||
console.error("Error starting Game Loop:", error);
|
||||
}
|
||||
} else {
|
||||
console.error("Loader: Game Loop module not found or start method missing.");
|
||||
// Hide overlay anyway, but log error
|
||||
hideOverlay();
|
||||
await hideOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -884,37 +876,178 @@ const ModuleLoader = (function() {
|
||||
* Then completely remove it from the DOM
|
||||
* @param {Function} [callback] - Optional callback to execute after fade completes
|
||||
*/
|
||||
function hideOverlay(callback) { // Added callback parameter
|
||||
async function hideOverlay(callback) { // Added callback parameter
|
||||
if (!loadingOverlay) {
|
||||
if (callback) callback(); // Call callback immediately if no overlay
|
||||
return;
|
||||
}
|
||||
|
||||
await waitForProgressIndicatorsToExit();
|
||||
|
||||
// Set opacity to 0 to trigger the fade-out transition
|
||||
loadingOverlay.style.opacity = '0';
|
||||
|
||||
// Use transition event listener to remove from DOM after fade completes
|
||||
loadingOverlay.addEventListener('transitionend', function handler(e) {
|
||||
// Only handle the opacity transition
|
||||
if (e.propertyName === 'opacity') {
|
||||
console.log('Module Loader: Removing overlay from DOM');
|
||||
await waitForTransition(loadingOverlay, 'opacity');
|
||||
|
||||
// Remove from DOM completely
|
||||
if (loadingOverlay.parentNode) {
|
||||
loadingOverlay.parentNode.removeChild(loadingOverlay);
|
||||
console.log('Module Loader: Removing overlay from DOM');
|
||||
|
||||
// Remove from DOM completely
|
||||
if (loadingOverlay.parentNode) {
|
||||
loadingOverlay.parentNode.removeChild(loadingOverlay);
|
||||
}
|
||||
|
||||
// Set to null to allow garbage collection
|
||||
loadingOverlay = null;
|
||||
|
||||
// Execute the callback if provided
|
||||
if (callback) callback();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate one module progress row out and resolve only after its own
|
||||
* fade/collapse animation has finished.
|
||||
* @param {string} moduleId - Module ID
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function animateModuleItemExit(moduleId) {
|
||||
if (moduleExitAnimations.has(moduleId)) {
|
||||
return moduleExitAnimations.get(moduleId);
|
||||
}
|
||||
|
||||
const moduleItem = document.getElementById(`module-${moduleId}`);
|
||||
if (!moduleItem) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const exitPromise = new Promise(resolve => {
|
||||
let settled = false;
|
||||
let timeoutId = null;
|
||||
|
||||
const finish = () => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
moduleItem.removeEventListener('animationend', handleAnimationEnd);
|
||||
if (moduleItem.parentNode) {
|
||||
moduleItem.parentNode.removeChild(moduleItem);
|
||||
}
|
||||
moduleExitAnimations.delete(moduleId);
|
||||
resolve();
|
||||
};
|
||||
|
||||
const handleAnimationEnd = event => {
|
||||
if (event.target === moduleItem && event.animationName === 'fadeOutModule') {
|
||||
finish();
|
||||
}
|
||||
};
|
||||
|
||||
// Let the finished status paint briefly before the row collapses.
|
||||
setTimeout(() => {
|
||||
if (!moduleItem.isConnected) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the event listener to prevent memory leaks
|
||||
loadingOverlay.removeEventListener('transitionend', handler);
|
||||
moduleItem.addEventListener('animationend', handleAnimationEnd);
|
||||
moduleItem.classList.add('module-finished');
|
||||
|
||||
// Set to null to allow garbage collection
|
||||
loadingOverlay = null;
|
||||
|
||||
// Execute the callback if provided
|
||||
if (callback) callback();
|
||||
}
|
||||
const animationTime = getLongestCssTime(moduleItem, 'animation');
|
||||
timeoutId = setTimeout(finish, Math.max(animationTime + 80, 80));
|
||||
}, 120);
|
||||
});
|
||||
|
||||
moduleExitAnimations.set(moduleId, exitPromise);
|
||||
return exitPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make every remaining progress row leave, then wait for all of them.
|
||||
* This keeps the overlay fade from racing the final row animations.
|
||||
*/
|
||||
async function waitForProgressIndicatorsToExit() {
|
||||
if (modulesList) {
|
||||
modulesList.querySelectorAll('.module-item').forEach(moduleItem => {
|
||||
const moduleId = moduleItem.id.replace(/^module-/, '');
|
||||
animateModuleItemExit(moduleId);
|
||||
});
|
||||
}
|
||||
|
||||
if (moduleExitAnimations.size > 0) {
|
||||
await Promise.allSettled([...moduleExitAnimations.values()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a CSS transition on an element. The timeout is derived from
|
||||
* computed CSS duration/delay so non-animated cases resolve immediately.
|
||||
* @param {Element} element - Element that is transitioning
|
||||
* @param {string} propertyName - CSS property to wait for
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function waitForTransition(element, propertyName) {
|
||||
const transitionTime = getLongestCssTime(element, 'transition');
|
||||
|
||||
if (transitionTime <= 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
let settled = false;
|
||||
const finish = () => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timeoutId);
|
||||
element.removeEventListener('transitionend', handleTransitionEnd);
|
||||
resolve();
|
||||
};
|
||||
const handleTransitionEnd = event => {
|
||||
if (event.target === element && event.propertyName === propertyName) {
|
||||
finish();
|
||||
}
|
||||
};
|
||||
const timeoutId = setTimeout(finish, transitionTime + 80);
|
||||
element.addEventListener('transitionend', handleTransitionEnd);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the longest duration+delay pair from computed transition/animation CSS.
|
||||
* @param {Element} element - Element to inspect
|
||||
* @param {'transition'|'animation'} kind - CSS timing group
|
||||
* @returns {number} milliseconds
|
||||
*/
|
||||
function getLongestCssTime(element, kind) {
|
||||
const style = window.getComputedStyle(element);
|
||||
const durations = parseCssTimeList(style[`${kind}Duration`]);
|
||||
const delays = parseCssTimeList(style[`${kind}Delay`]);
|
||||
const count = Math.max(durations.length, delays.length);
|
||||
let longest = 0;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const duration = durations[i % durations.length] || 0;
|
||||
const delay = delays[i % delays.length] || 0;
|
||||
longest = Math.max(longest, duration + delay);
|
||||
}
|
||||
|
||||
return longest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a comma separated CSS time list into milliseconds.
|
||||
* @param {string} value - CSS time list
|
||||
* @returns {number[]}
|
||||
*/
|
||||
function parseCssTimeList(value) {
|
||||
return String(value || '0s').split(',').map(part => {
|
||||
const text = part.trim();
|
||||
const amount = Number.parseFloat(text);
|
||||
if (!Number.isFinite(amount)) return 0;
|
||||
return text.endsWith('ms') ? amount : amount * 1000;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user