,
This commit is contained in:
parent
70423851ba
commit
1fe5015056
55 changed files with 2036 additions and 685 deletions
151
src/index.ts
151
src/index.ts
|
|
@ -2,6 +2,13 @@ import GameLoop from './game-loop/game-loop';
|
|||
|
||||
import './index.scss';
|
||||
|
||||
import {
|
||||
initAnalytics,
|
||||
trackExport,
|
||||
trackSettingsOpen,
|
||||
trackVibeChange,
|
||||
} from './analytics';
|
||||
import { preloadPianoSamples } from './audio/piano-samples';
|
||||
import { appConfig } from './config';
|
||||
import { CollapsiblePanelAnimator } from './page/collapsible-panel-animator';
|
||||
import { ConfigPane } from './page/config-pane';
|
||||
|
|
@ -11,7 +18,12 @@ import { activeVibe, applyVibeSettings, resetSettings, settings } from './settin
|
|||
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 {
|
||||
ErrorHandler,
|
||||
getErrorMessage,
|
||||
RuntimeError,
|
||||
Severity,
|
||||
} from './utils/error-handler';
|
||||
import { initializeGpu } from './utils/graphics/initialize-gpu';
|
||||
import { VIBE_PRESETS } from './vibes';
|
||||
|
||||
|
|
@ -47,10 +59,11 @@ const formatMirrorSegmentCount = (count: number): string =>
|
|||
? 'Mirror off'
|
||||
: `${count} ${mirrorSegmentNames[count] ?? 'slices'}`;
|
||||
|
||||
const renderRuntimeMessage = (
|
||||
container: HTMLElement,
|
||||
error: Parameters<Parameters<typeof ErrorHandler.addOnErrorListener>[0]>[0]
|
||||
) => {
|
||||
type RuntimeUiError = Parameters<
|
||||
Parameters<typeof ErrorHandler.addOnErrorListener>[0]
|
||||
>[0];
|
||||
|
||||
const renderRuntimeMessage = (container: HTMLElement, error: RuntimeUiError) => {
|
||||
const message = document.createElement('pre');
|
||||
message.className = error.severity;
|
||||
message.textContent = error.code ? `${error.message}\n${error.code}` : error.message;
|
||||
|
|
@ -67,8 +80,31 @@ const renderRuntimeMessage = (
|
|||
}
|
||||
};
|
||||
|
||||
const elements = {
|
||||
const getRuntimeUiError = (exception: unknown): RuntimeUiError => ({
|
||||
severity: Severity.ERROR,
|
||||
message: getErrorMessage(exception),
|
||||
...(exception instanceof RuntimeError ? { code: exception.code } : {}),
|
||||
});
|
||||
|
||||
const renderStartupException = (exception: unknown) => {
|
||||
const existingContainer = document.querySelector('.errors-container');
|
||||
const container =
|
||||
existingContainer instanceof HTMLElement
|
||||
? existingContainer
|
||||
: document.createElement('div');
|
||||
|
||||
if (!(existingContainer instanceof HTMLElement)) {
|
||||
container.className = 'errors-container';
|
||||
document.body.append(container);
|
||||
}
|
||||
|
||||
container.setAttribute('aria-live', 'assertive');
|
||||
renderRuntimeMessage(container, getRuntimeUiError(exception));
|
||||
};
|
||||
|
||||
const queryAppElements = () => ({
|
||||
aside: queryRequiredElement('aside', HTMLElement),
|
||||
toolbarRow: queryRequiredElement('.toolbar-row', HTMLElement),
|
||||
infoButton: queryRequiredElement('button.info', HTMLButtonElement),
|
||||
infoElement: queryRequiredElement('.info-page', HTMLElement),
|
||||
minimizeFullScreenButton: queryRequiredElement(
|
||||
|
|
@ -98,7 +134,11 @@ const elements = {
|
|||
loadingIndicator: queryRequiredElement('.loading-indicator', HTMLDivElement),
|
||||
loadingStatus: queryRequiredElement('.loading-status', HTMLDivElement),
|
||||
loadingProgress: queryRequiredElement('.loading-progress', HTMLDivElement),
|
||||
};
|
||||
});
|
||||
|
||||
type AppElements = ReturnType<typeof queryAppElements>;
|
||||
|
||||
let elements: AppElements;
|
||||
|
||||
const setLoadingStage = (label: string, ratio: number) => {
|
||||
const percent = Math.round(Math.max(0, Math.min(1, ratio)) * 100);
|
||||
|
|
@ -188,10 +228,15 @@ const renderMirrorSegmentUi = () => {
|
|||
};
|
||||
|
||||
const main = async () => {
|
||||
let hasRuntimeErrorListener = false;
|
||||
try {
|
||||
initAnalytics();
|
||||
|
||||
let shouldStop = false;
|
||||
let game: GameLoop | null = null;
|
||||
let wasConfigPaneOpen = false;
|
||||
|
||||
elements = queryAppElements();
|
||||
elements.errorContainer.setAttribute('aria-live', 'assertive');
|
||||
ErrorHandler.addOnErrorListener((error, _metadata) => {
|
||||
renderRuntimeMessage(elements.errorContainer, error);
|
||||
|
|
@ -201,6 +246,7 @@ const main = async () => {
|
|||
shouldStop = true;
|
||||
}
|
||||
});
|
||||
hasRuntimeErrorListener = true;
|
||||
|
||||
const syncRuntimeUi = () => {
|
||||
renderEraserSizeUi(game);
|
||||
|
|
@ -216,7 +262,13 @@ const main = async () => {
|
|||
const configPane = new ConfigPane({
|
||||
settingsButton: elements.settingsButton,
|
||||
onConfigChange: syncRuntimeUi,
|
||||
onOpenChange: (isOpen) => game?.setStatsOverlayPinned(isOpen),
|
||||
onOpenChange: (isOpen) => {
|
||||
game?.setStatsOverlayPinned(isOpen);
|
||||
if (isOpen && !wasConfigPaneOpen) {
|
||||
trackSettingsOpen();
|
||||
}
|
||||
wasConfigPaneOpen = isOpen;
|
||||
},
|
||||
onRuntimeChange: syncRuntimeUi,
|
||||
onRuntimeReset: () => {
|
||||
resetSettings();
|
||||
|
|
@ -224,7 +276,12 @@ const main = async () => {
|
|||
},
|
||||
onRestart: () => game?.destroy(),
|
||||
onVibeChange: (vibeId) => {
|
||||
applyVibeSettings(vibeId);
|
||||
const vibe = applyVibeSettings(vibeId);
|
||||
trackVibeChange({
|
||||
vibeId: vibe.id,
|
||||
vibeName: vibe.name,
|
||||
source: 'settings',
|
||||
});
|
||||
syncRuntimeUi();
|
||||
game?.playVibeChangeAudio(false);
|
||||
},
|
||||
|
|
@ -244,33 +301,65 @@ const main = async () => {
|
|||
document.body
|
||||
);
|
||||
|
||||
const startAudioFromUserGesture = (event: Event) => {
|
||||
if (
|
||||
isAudioMuted ||
|
||||
(event.target instanceof Node && elements.soundButton.contains(event.target))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
game?.startAudio(true);
|
||||
};
|
||||
|
||||
window.addEventListener('touchend', startAudioFromUserGesture, {
|
||||
capture: true,
|
||||
passive: true,
|
||||
});
|
||||
window.addEventListener('pointerup', startAudioFromUserGesture, {
|
||||
capture: true,
|
||||
passive: true,
|
||||
});
|
||||
window.addEventListener('click', startAudioFromUserGesture, { capture: true });
|
||||
window.addEventListener('keydown', startAudioFromUserGesture, { capture: true });
|
||||
|
||||
elements.restartButton.addEventListener('click', () => game?.destroy());
|
||||
elements.soundButton.addEventListener('click', (event) => {
|
||||
elements.soundButton.addEventListener('click', () => {
|
||||
isAudioMuted = !isAudioMuted;
|
||||
writeBrowserStorage(appConfig.storage.audioMutedKey, isAudioMuted ? '1' : '0');
|
||||
renderAudioUi(game);
|
||||
if (!isAudioMuted) {
|
||||
game?.startAudio(event.isTrusted);
|
||||
game?.startAudio(true);
|
||||
}
|
||||
});
|
||||
|
||||
elements.previousVibe.addEventListener('click', (event) => {
|
||||
elements.previousVibe.addEventListener('click', () => {
|
||||
const current = VIBE_PRESETS.findIndex((vibe) => vibe.id === activeVibe.id);
|
||||
const vibe =
|
||||
VIBE_PRESETS[(current + VIBE_PRESETS.length - 1) % VIBE_PRESETS.length];
|
||||
applyVibeSettings(vibe.id);
|
||||
const activePreset = applyVibeSettings(vibe.id);
|
||||
trackVibeChange({
|
||||
vibeId: activePreset.id,
|
||||
vibeName: activePreset.name,
|
||||
source: 'previous-button',
|
||||
});
|
||||
configPane.refresh();
|
||||
syncRuntimeUi();
|
||||
game?.playVibeChangeAudio(event.isTrusted);
|
||||
game?.playVibeChangeAudio(true);
|
||||
});
|
||||
|
||||
elements.nextVibe.addEventListener('click', (event) => {
|
||||
elements.nextVibe.addEventListener('click', () => {
|
||||
const current = VIBE_PRESETS.findIndex((vibe) => vibe.id === activeVibe.id);
|
||||
const vibe = VIBE_PRESETS[(current + 1) % VIBE_PRESETS.length];
|
||||
applyVibeSettings(vibe.id);
|
||||
const activePreset = applyVibeSettings(vibe.id);
|
||||
trackVibeChange({
|
||||
vibeId: activePreset.id,
|
||||
vibeName: activePreset.name,
|
||||
source: 'next-button',
|
||||
});
|
||||
configPane.refresh();
|
||||
syncRuntimeUi();
|
||||
game?.playVibeChangeAudio(event.isTrusted);
|
||||
game?.playVibeChangeAudio(true);
|
||||
});
|
||||
|
||||
elements.swatches.forEach((swatch, index) => {
|
||||
|
|
@ -318,6 +407,7 @@ const main = async () => {
|
|||
elements.export4k.disabled = true;
|
||||
try {
|
||||
await game.export4K();
|
||||
trackExport({ vibeId: activeVibe.id });
|
||||
} catch (error) {
|
||||
ErrorHandler.addException(error, { severity: Severity.WARNING });
|
||||
} finally {
|
||||
|
|
@ -330,18 +420,32 @@ const main = async () => {
|
|||
renderMirrorSegmentUi();
|
||||
renderAudioUi(game);
|
||||
|
||||
const fontsReady = document.fonts.ready.catch(() => undefined);
|
||||
const fontsReady = document.fonts.ready.catch((error) => {
|
||||
ErrorHandler.addException(error, {
|
||||
fallbackMessage: 'Could not load fonts.',
|
||||
severity: Severity.WARNING,
|
||||
});
|
||||
});
|
||||
setLoadingStage('Connecting to GPU…', 0.1);
|
||||
const gpu = await initializeGpu();
|
||||
setLoadingStage('Loading fonts…', 0.4);
|
||||
setLoadingStage('Loading fonts…', 0.3);
|
||||
await fontsReady;
|
||||
setLoadingStage('Compiling shaders…', 0.7);
|
||||
setLoadingStage('Loading piano samples…', 0.45);
|
||||
await preloadPianoSamples(({ loadedCount, totalCount }) => {
|
||||
const sampleRatio = totalCount > 0 ? loadedCount / totalCount : 1;
|
||||
setLoadingStage(
|
||||
`Loading piano samples ${loadedCount}/${totalCount}…`,
|
||||
0.45 + sampleRatio * 0.3
|
||||
);
|
||||
});
|
||||
setLoadingStage('Compiling shaders…', 0.8);
|
||||
|
||||
const deltaTimeCalculator = new DeltaTimeCalculator();
|
||||
|
||||
let isFirstStart = true;
|
||||
while (!shouldStop) {
|
||||
game = new GameLoop(elements.canvas, gpu, deltaTimeCalculator, {
|
||||
toolbar: elements.toolbarRow,
|
||||
prompt: elements.prompt,
|
||||
eraserPreview: elements.eraserPreview,
|
||||
exportStatus: elements.exportStatus,
|
||||
|
|
@ -364,7 +468,12 @@ const main = async () => {
|
|||
}
|
||||
} catch (e) {
|
||||
document.body.classList.remove('is-loading');
|
||||
ErrorHandler.addException(e);
|
||||
if (hasRuntimeErrorListener) {
|
||||
ErrorHandler.addException(e);
|
||||
} else {
|
||||
renderStartupException(e);
|
||||
ErrorHandler.addException(e);
|
||||
}
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue