.
Some checks failed
Deploy to Pages / build (pull_request) Failing after 3m15s

This commit is contained in:
Andras Schmelczer 2026-05-13 22:13:15 +01:00
commit 2347ecd201
71 changed files with 3779 additions and 1586 deletions

View file

@ -8,7 +8,9 @@ import { ConfigPane } from './page/config-pane';
import { FullScreenHandler } from './page/full-screen-handler';
import { MenuHider } from './page/menu-hider';
import { activeVibe, applyVibeSettings, resetSettings, settings } from './settings';
import { readBrowserStorage, writeBrowserStorage } from './utils/browser-storage';
import { DeltaTimeCalculator } from './utils/delta-time-calculator';
import { queryRequiredElement, queryRequiredElements } from './utils/dom';
import { ErrorHandler, Severity } from './utils/error-handler';
import { initializeGpu } from './utils/graphics/initialize-gpu';
import { VIBE_PRESETS } from './vibes';
@ -37,10 +39,13 @@ const getMirrorSegmentRatio = (count: number): number =>
(count - appConfig.toolbar.mirror.min) /
(appConfig.toolbar.mirror.max - appConfig.toolbar.mirror.min);
const mirrorSegmentNames: Readonly<Record<number, string>> =
appConfig.toolbar.mirror.names;
const formatMirrorSegmentCount = (count: number): string =>
count === appConfig.toolbar.mirror.default
? 'Mirror off'
: `${count} ${appConfig.toolbar.mirror.names[count] ?? 'slices'}`;
: `${count} ${mirrorSegmentNames[count] ?? 'slices'}`;
const renderRuntimeMessage = (
container: HTMLElement,
@ -63,40 +68,52 @@ const renderRuntimeMessage = (
};
const elements = {
aside: document.querySelector('aside') as HTMLDivElement,
infoButton: document.querySelector('button.info') as HTMLButtonElement,
infoElement: document.querySelector('.info-page') as HTMLDivElement,
minimizeFullScreenButton: document.querySelector(
'button.minimize-full-screen'
) as HTMLButtonElement,
maximizeFullScreenButton: document.querySelector(
'button.maximize-full-screen'
) as HTMLButtonElement,
settingsButton: document.querySelector('button.settings') as HTMLButtonElement,
soundButton: document.querySelector('button.sound') as HTMLButtonElement,
restartButton: document.querySelector('button.restart') as HTMLButtonElement,
canvas: document.querySelector('canvas') as HTMLCanvasElement,
eraserPreview: document.querySelector('.eraser-preview') as HTMLDivElement,
errorContainer: document.querySelector('.errors-container') as HTMLDivElement,
previousVibe: document.querySelector('.previous-vibe') as HTMLButtonElement,
nextVibe: document.querySelector('.next-vibe') as HTMLButtonElement,
swatches: Array.from(
document.querySelectorAll('.color-swatch')
) as Array<HTMLButtonElement>,
eraserSizeControl: document.querySelector('.eraser-size-control') as HTMLLabelElement,
eraserSizeSlider: document.querySelector('.eraser-size-slider') as HTMLInputElement,
mirrorSegmentControl: document.querySelector(
'.mirror-segment-control'
) as HTMLLabelElement,
mirrorSegmentSlider: document.querySelector(
'.mirror-segment-slider'
) as HTMLInputElement,
export4k: document.querySelector('.export-4k') as HTMLButtonElement,
exportStatus: document.querySelector('.export-status') as HTMLSpanElement,
prompt: document.querySelector('.garden-prompt') as HTMLDivElement,
aside: queryRequiredElement('aside', HTMLDivElement),
infoButton: queryRequiredElement('button.info', HTMLButtonElement),
infoElement: queryRequiredElement('.info-page', HTMLDivElement),
minimizeFullScreenButton: queryRequiredElement(
'button.minimize-full-screen',
HTMLButtonElement
),
maximizeFullScreenButton: queryRequiredElement(
'button.maximize-full-screen',
HTMLButtonElement
),
settingsButton: queryRequiredElement('button.settings', HTMLButtonElement),
soundButton: queryRequiredElement('button.sound', HTMLButtonElement),
restartButton: queryRequiredElement('button.restart', HTMLButtonElement),
canvas: queryRequiredElement('canvas', HTMLCanvasElement),
eraserPreview: queryRequiredElement('.eraser-preview', HTMLDivElement),
errorContainer: queryRequiredElement('.errors-container', HTMLDivElement),
previousVibe: queryRequiredElement('.previous-vibe', HTMLButtonElement),
nextVibe: queryRequiredElement('.next-vibe', HTMLButtonElement),
swatches: queryRequiredElements('.color-swatch', HTMLButtonElement),
eraserSizeControl: queryRequiredElement('.eraser-size-control', HTMLLabelElement),
eraserSizeSlider: queryRequiredElement('.eraser-size-slider', HTMLInputElement),
mirrorSegmentControl: queryRequiredElement(
'.mirror-segment-control',
HTMLLabelElement
),
mirrorSegmentSlider: queryRequiredElement(
'.mirror-segment-slider',
HTMLInputElement
),
export4k: queryRequiredElement('.export-4k', HTMLButtonElement),
exportStatus: queryRequiredElement('.export-status', HTMLSpanElement),
prompt: queryRequiredElement('.garden-prompt', HTMLDivElement),
loadingIndicator: queryRequiredElement('.loading-indicator', HTMLDivElement),
loadingStatus: queryRequiredElement('.loading-status', HTMLDivElement),
loadingProgress: queryRequiredElement('.loading-progress', HTMLDivElement),
};
let isAudioMuted = localStorage.getItem(appConfig.storage.audioMutedKey) === '1';
const setLoadingStage = (label: string, ratio: number) => {
const percent = Math.round(Math.max(0, Math.min(1, ratio)) * 100);
elements.loadingStatus.textContent = label;
elements.loadingIndicator.style.setProperty('--loading-progress', `${percent}%`);
elements.loadingProgress.setAttribute('aria-valuenow', String(percent));
};
let isAudioMuted = readBrowserStorage(appConfig.storage.audioMutedKey) === '1';
const renderAudioUi = (game: GameLoop | null) => {
elements.soundButton.classList.toggle('muted', isAudioMuted);
@ -185,6 +202,7 @@ const main = async () => {
ErrorHandler.addOnErrorListener((error, _metadata) => {
renderRuntimeMessage(elements.errorContainer, error);
if (error.severity === Severity.ERROR) {
document.body.classList.remove('is-loading');
game?.destroy();
shouldStop = true;
}
@ -223,7 +241,8 @@ const main = async () => {
() =>
FullScreenHandler.isInFullScreenMode() &&
!configPane.isOpen &&
!infoPageHandler.isOpen
!infoPageHandler.isOpen,
{ persistentElement: elements.settingsButton }
);
new FullScreenHandler(
elements.minimizeFullScreenButton,
@ -232,13 +251,16 @@ const main = async () => {
);
const fontsReady = document.fonts.ready.catch(() => undefined);
setLoadingStage('Connecting to GPU…', 0.1);
const gpu = await initializeGpu();
setLoadingStage('Loading fonts…', 0.4);
await fontsReady;
setLoadingStage('Compiling shaders…', 0.7);
elements.restartButton.addEventListener('click', () => game?.destroy());
elements.soundButton.addEventListener('click', (event) => {
isAudioMuted = !isAudioMuted;
localStorage.setItem(appConfig.storage.audioMutedKey, isAudioMuted ? '1' : '0');
writeBrowserStorage(appConfig.storage.audioMutedKey, isAudioMuted ? '1' : '0');
renderAudioUi(game);
if (!isAudioMuted) {
game?.startAudio(event.isTrusted);
@ -323,6 +345,7 @@ const main = async () => {
renderMirrorSegmentUi();
renderAudioUi(game);
let isFirstStart = true;
while (!shouldStop) {
game = new GameLoop(elements.canvas, gpu, deltaTimeCalculator, {
prompt: elements.prompt,
@ -334,9 +357,18 @@ const main = async () => {
renderMirrorSegmentUi();
renderAudioUi(game);
await game.start();
const startPromise = game.start();
if (isFirstStart) {
isFirstStart = false;
setLoadingStage('Ready', 1);
requestAnimationFrame(() =>
requestAnimationFrame(() => document.body.classList.remove('is-loading'))
);
}
await startPromise;
}
} catch (e) {
document.body.classList.remove('is-loading');
ErrorHandler.addException(e);
console.error(e);
}