Stabilize playback state and cursor feedback

This commit is contained in:
2026-05-18 20:57:20 +02:00
parent 6e908037fb
commit 751ac5f62b
13 changed files with 580 additions and 82 deletions
+174 -41
View File
@@ -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;
});
}
/**