This commit is contained in:
Andras Schmelczer 2026-05-17 14:12:01 +01:00
commit 80ed37298b
73 changed files with 3619 additions and 1295 deletions

View file

@ -11,6 +11,7 @@ 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 { clamp } from './utils/clamp';
import { DeltaTimeCalculator } from './utils/delta-time-calculator';
import { queryRequiredElement, queryRequiredElements } from './utils/dom';
import {
@ -51,8 +52,30 @@ const mirrorSegmentNames: Readonly<Record<number, string>> =
const formatMirrorSegmentCount = (count: number): string =>
count === appConfig.toolbar.mirror.default
? 'Mirror off'
: `${count} ${mirrorSegmentNames[count] ?? 'slices'}`;
? appConfig.toolbar.mirror.offLabel
: `${count} ${mirrorSegmentNames[count] ?? appConfig.toolbar.mirror.fallbackSegmentName}`;
const clampAudioVolume = (value: number): number => {
const safeValue = Number.isFinite(value) ? value : appConfig.toolbar.volume.default;
return clamp(safeValue, appConfig.toolbar.volume.min, appConfig.toolbar.volume.max);
};
const getAudioVolumeRatio = (volume: number): number =>
(volume - appConfig.toolbar.volume.min) /
(appConfig.toolbar.volume.max - appConfig.toolbar.volume.min);
const getAudioVolumePercent = (volume: number): number =>
Math.round(getAudioVolumeRatio(volume) * 100);
const readInitialAudioVolume = (): number => {
const storedVolume = readBrowserStorage(appConfig.storage.audioVolumeKey);
return storedVolume === null
? appConfig.toolbar.volume.default
: clampAudioVolume(Number(storedVolume));
};
const formatStoredAudioVolume = (volume: number): string =>
clampAudioVolume(volume).toFixed(2);
type RuntimeUiError = Parameters<
Parameters<typeof ErrorHandler.addOnErrorListener>[0]
@ -112,6 +135,8 @@ const queryAppElements = () => ({
),
settingsButton: queryRequiredElement('button.settings', HTMLButtonElement),
soundButton: queryRequiredElement('button.sound', HTMLButtonElement),
volumeControl: queryRequiredElement('.volume-control', HTMLLabelElement),
volumeSlider: queryRequiredElement('.volume-slider', HTMLInputElement),
restartButton: queryRequiredElement('button.restart', HTMLButtonElement),
canvas: queryRequiredElement('canvas', HTMLCanvasElement),
eraserPreview: queryRequiredElement('.eraser-preview', HTMLDivElement),
@ -141,18 +166,49 @@ const setLoadingStage = (label: string, ratio: number) => {
elements.loadingProgress.setAttribute('aria-valuenow', String(percent));
};
let isAudioMuted = readBrowserStorage(appConfig.storage.audioMutedKey) === '1';
let audioVolume = readInitialAudioVolume();
let isAudioMuted =
readBrowserStorage(appConfig.storage.audioMutedKey) === '1' ||
audioVolume <= appConfig.toolbar.volume.min;
let isEraserActive = false;
const persistAudioUiState = () => {
writeBrowserStorage(appConfig.storage.audioMutedKey, isAudioMuted ? '1' : '0');
writeBrowserStorage(
appConfig.storage.audioVolumeKey,
formatStoredAudioVolume(audioVolume)
);
};
const renderAudioUi = (game: GameLoop | null) => {
elements.soundButton.classList.toggle('muted', isAudioMuted);
elements.soundButton.setAttribute('aria-pressed', String(isAudioMuted));
audioVolume = clampAudioVolume(audioVolume);
const isEffectivelyMuted = isAudioMuted || audioVolume <= appConfig.toolbar.volume.min;
const volumePercent = getAudioVolumePercent(audioVolume);
elements.soundButton.classList.toggle('muted', isEffectivelyMuted);
elements.soundButton.setAttribute('aria-pressed', String(isEffectivelyMuted));
elements.soundButton.setAttribute(
'aria-label',
isAudioMuted ? 'Unmute audio' : 'Mute audio'
isEffectivelyMuted ? 'Unmute audio' : 'Mute audio'
);
elements.soundButton.title = isAudioMuted ? 'Unmute audio' : 'Mute audio';
game?.setAudioMuted(isAudioMuted);
elements.soundButton.title = isEffectivelyMuted ? 'Unmute audio' : 'Mute audio';
elements.volumeSlider.min = appConfig.toolbar.volume.min.toString();
elements.volumeSlider.max = appConfig.toolbar.volume.max.toString();
elements.volumeSlider.step = appConfig.toolbar.volume.step.toString();
elements.volumeSlider.value = formatStoredAudioVolume(audioVolume);
elements.volumeSlider.setAttribute(
'aria-valuetext',
isEffectivelyMuted ? `Muted, ${volumePercent}%` : `${volumePercent}%`
);
elements.volumeControl.classList.toggle('muted', isEffectivelyMuted);
elements.volumeControl.title = isEffectivelyMuted
? `Muted, ${volumePercent}% volume`
: `${volumePercent}% volume`;
elements.volumeControl.style.setProperty('--volume-progress', `${volumePercent}%`);
game?.setAudioVolume(audioVolume);
game?.setAudioMuted(isEffectivelyMuted);
};
const renderPaletteUi = (game: GameLoop | null) => {
@ -322,8 +378,21 @@ const main = async () => {
elements.restartButton.addEventListener('click', () => game?.destroy());
elements.soundButton.addEventListener('click', () => {
isAudioMuted = !isAudioMuted;
writeBrowserStorage(appConfig.storage.audioMutedKey, isAudioMuted ? '1' : '0');
const shouldUnmute = isAudioMuted || audioVolume <= appConfig.toolbar.volume.min;
if (shouldUnmute && audioVolume <= appConfig.toolbar.volume.min) {
audioVolume = appConfig.toolbar.volume.default;
}
isAudioMuted = !shouldUnmute;
persistAudioUiState();
renderAudioUi(game);
if (!isAudioMuted) {
game?.startAudio(true);
}
});
elements.volumeSlider.addEventListener('input', () => {
audioVolume = clampAudioVolume(Number(elements.volumeSlider.value));
isAudioMuted = audioVolume <= appConfig.toolbar.volume.min;
persistAudioUiState();
renderAudioUi(game);
if (!isAudioMuted) {
game?.startAudio(true);