Clean up configs
This commit is contained in:
parent
79638d5fa4
commit
0fddad6b45
7 changed files with 375 additions and 652 deletions
|
|
@ -1,19 +1,24 @@
|
|||
import { appConfig } from '../config';
|
||||
import { DEFAULT_AUDIO_VOLUME } from '../audio/garden-audio-config';
|
||||
import type GameLoop from '../game-loop/game-loop';
|
||||
import { readBrowserStorage, writeBrowserStorage } from '../utils/browser-storage';
|
||||
import { queryRequiredElement } from '../utils/dom';
|
||||
import { clamp01 } from '../utils/math';
|
||||
|
||||
const AUDIO_MUTED_STORAGE_KEY = 'fleeting-garden:audio-muted';
|
||||
const AUDIO_VOLUME_STORAGE_KEY = 'fleeting-garden:audio-volume';
|
||||
const AUDIO_VOLUME_MIN = 0;
|
||||
const AUDIO_VOLUME_MAX = 1;
|
||||
const AUDIO_VOLUME_STEP = 0.01;
|
||||
|
||||
const clampAudioVolume = (value: number): number => {
|
||||
const { default: defaultVolume, max, min } = appConfig.toolbar.volume;
|
||||
const safeValue = Number.isFinite(value) ? value : defaultVolume;
|
||||
return Math.min(max, Math.max(min, clamp01(safeValue)));
|
||||
const safeValue = Number.isFinite(value) ? value : DEFAULT_AUDIO_VOLUME;
|
||||
return Math.min(AUDIO_VOLUME_MAX, Math.max(AUDIO_VOLUME_MIN, clamp01(safeValue)));
|
||||
};
|
||||
|
||||
const readInitialAudioVolume = (): number => {
|
||||
const storedVolume = readBrowserStorage(appConfig.storage.audioVolumeKey);
|
||||
const storedVolume = readBrowserStorage(AUDIO_VOLUME_STORAGE_KEY);
|
||||
return storedVolume === null
|
||||
? appConfig.toolbar.volume.default
|
||||
? DEFAULT_AUDIO_VOLUME
|
||||
: clampAudioVolume(Number(storedVolume));
|
||||
};
|
||||
|
||||
|
|
@ -45,7 +50,7 @@ export class AudioControl {
|
|||
|
||||
private audioVolume = readInitialAudioVolume();
|
||||
private isMutedState =
|
||||
readBrowserStorage(appConfig.storage.audioMutedKey) === STORED_MUTED_TRUE ||
|
||||
readBrowserStorage(AUDIO_MUTED_STORAGE_KEY) === STORED_MUTED_TRUE ||
|
||||
this.audioVolume <= 0;
|
||||
|
||||
public constructor(private readonly options: AudioControlOptions) {
|
||||
|
|
@ -85,9 +90,9 @@ export class AudioControl {
|
|||
this.soundButton.setAttribute('aria-label', muteLabel);
|
||||
this.soundButton.title = muteLabel;
|
||||
|
||||
this.volumeSlider.min = appConfig.toolbar.volume.min.toString();
|
||||
this.volumeSlider.max = appConfig.toolbar.volume.max.toString();
|
||||
this.volumeSlider.step = appConfig.toolbar.volume.step.toString();
|
||||
this.volumeSlider.min = AUDIO_VOLUME_MIN.toString();
|
||||
this.volumeSlider.max = AUDIO_VOLUME_MAX.toString();
|
||||
this.volumeSlider.step = AUDIO_VOLUME_STEP.toString();
|
||||
this.volumeSlider.value = formatStoredAudioVolume(this.audioVolume);
|
||||
this.volumeSlider.setAttribute(
|
||||
'aria-valuetext',
|
||||
|
|
@ -107,7 +112,7 @@ export class AudioControl {
|
|||
private readonly onToggleMute = () => {
|
||||
const shouldUnmute = this.isMutedState || this.audioVolume <= 0;
|
||||
if (shouldUnmute && this.audioVolume <= 0) {
|
||||
this.audioVolume = appConfig.toolbar.volume.default;
|
||||
this.audioVolume = DEFAULT_AUDIO_VOLUME;
|
||||
}
|
||||
this.isMutedState = !shouldUnmute;
|
||||
this.persist();
|
||||
|
|
@ -141,11 +146,11 @@ export class AudioControl {
|
|||
|
||||
private persist(): void {
|
||||
writeBrowserStorage(
|
||||
appConfig.storage.audioMutedKey,
|
||||
AUDIO_MUTED_STORAGE_KEY,
|
||||
this.isMutedState ? STORED_MUTED_TRUE : STORED_MUTED_FALSE
|
||||
);
|
||||
writeBrowserStorage(
|
||||
appConfig.storage.audioVolumeKey,
|
||||
AUDIO_VOLUME_STORAGE_KEY,
|
||||
formatStoredAudioVolume(this.audioVolume)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { FolderApi } from '@tweakpane/core';
|
||||
|
||||
import { appConfig, normalizeNumberControlValue } from '../config';
|
||||
import { normalizeNumberControlValue, runtimeControls } from '../config';
|
||||
import { activeVibe, settings } from '../settings';
|
||||
import { rgbColorToCss } from '../utils/rgb-color';
|
||||
|
||||
|
|
@ -128,7 +128,7 @@ export class ColorReactionMatrixControl {
|
|||
const cell = document.createElement('div');
|
||||
cell.className = 'color-reaction-matrix__cell';
|
||||
|
||||
const config = appConfig.runtimeSettings.controls[key];
|
||||
const config = runtimeControls[key];
|
||||
if (!config) {
|
||||
return cell;
|
||||
}
|
||||
|
|
@ -165,7 +165,7 @@ export class ColorReactionMatrixControl {
|
|||
sourceColorIndex: number,
|
||||
targetColorIndex: number
|
||||
): void {
|
||||
const config = appConfig.runtimeSettings.controls[key];
|
||||
const config = runtimeControls[key];
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@ import { Pane } from 'tweakpane';
|
|||
|
||||
import type { GardenAudioVibeSettings } from '../audio/garden-audio-config';
|
||||
import {
|
||||
appConfig,
|
||||
normalizeNumberControlValue,
|
||||
runtimeControls,
|
||||
type GardenRuntimeSettings,
|
||||
type NumberControlConfig,
|
||||
} from '../config';
|
||||
import { perfStatsOverlayState } from '../game-loop/perf-stats-overlay';
|
||||
import { activeVibe, settings } from '../settings';
|
||||
import { hexColorToRgbColor, rgbColorToHex, type RgbColor } from '../utils/rgb-color';
|
||||
import { ColorReactionMatrixControl } from './color-reaction-matrix-control';
|
||||
|
|
@ -29,6 +30,8 @@ interface PaneState extends GardenAudioVibeSettings {
|
|||
}
|
||||
|
||||
const runtimeFolderOrder = ['Brush', 'Movement', 'Look', 'Performance'] as const;
|
||||
const CONFIG_PANE_TITLE = 'Garden Settings';
|
||||
const CONFIG_PANE_START_HIDDEN = true;
|
||||
|
||||
const MUSIC_CONTROLS: ReadonlyArray<{
|
||||
key: VibeNumberKey;
|
||||
|
|
@ -56,7 +59,7 @@ interface ConfigPaneOptions {
|
|||
|
||||
const getRuntimeControlKeys = (folder: string): Array<RuntimeControlKey> =>
|
||||
(
|
||||
Object.entries(appConfig.runtimeSettings.controls) as Array<
|
||||
Object.entries(runtimeControls) as Array<
|
||||
[RuntimeControlKey, NumberControlConfig | undefined]
|
||||
>
|
||||
)
|
||||
|
|
@ -123,10 +126,10 @@ export class ConfigPane {
|
|||
|
||||
this.pane = new Pane({
|
||||
container: this.container,
|
||||
title: appConfig.tuningPane.title,
|
||||
title: CONFIG_PANE_TITLE,
|
||||
expanded: true,
|
||||
});
|
||||
this.pane.hidden = appConfig.tuningPane.startHidden;
|
||||
this.pane.hidden = CONFIG_PANE_START_HIDDEN;
|
||||
this.pane.element.classList.add('config-pane');
|
||||
this.pane.element.id = 'config-pane';
|
||||
|
||||
|
|
@ -310,7 +313,7 @@ export class ConfigPane {
|
|||
private getRuntimeControlConfig(
|
||||
key: RuntimeControlKey
|
||||
): NumberControlConfig | undefined {
|
||||
const config = appConfig.runtimeSettings.controls[key];
|
||||
const config = runtimeControls[key];
|
||||
if (!config || key !== 'maxAgentCount') {
|
||||
return config;
|
||||
}
|
||||
|
|
@ -323,7 +326,7 @@ export class ConfigPane {
|
|||
|
||||
private addFpsOverlayBinding(container: PaneContainer): void {
|
||||
container
|
||||
.addBinding(appConfig.tuningPane, 'showFpsOverlay', {
|
||||
.addBinding(perfStatsOverlayState, 'isVisible', {
|
||||
label: 'Show FPS',
|
||||
})
|
||||
.on('change', () => this.options.onConfigChange());
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { appConfig } from '../config';
|
||||
import {
|
||||
ERASER_SIZE_MAX,
|
||||
ERASER_SIZE_MIN,
|
||||
getEraserSizeFromSliderRatio,
|
||||
getEraserSliderRatioFromSize,
|
||||
} from './eraser-size-control';
|
||||
|
||||
describe('eraser size slider mapping', () => {
|
||||
it('maps slider position quadratically to eraser size', () => {
|
||||
const { max, min } = appConfig.toolbar.eraser;
|
||||
|
||||
expect(getEraserSizeFromSliderRatio(0)).toBe(min);
|
||||
expect(getEraserSizeFromSliderRatio(0.5)).toBe(min + (max - min) * 0.25);
|
||||
expect(getEraserSizeFromSliderRatio(1)).toBe(max);
|
||||
expect(getEraserSizeFromSliderRatio(0)).toBe(ERASER_SIZE_MIN);
|
||||
expect(getEraserSizeFromSliderRatio(0.5)).toBe(
|
||||
ERASER_SIZE_MIN + (ERASER_SIZE_MAX - ERASER_SIZE_MIN) * 0.25
|
||||
);
|
||||
expect(getEraserSizeFromSliderRatio(1)).toBe(ERASER_SIZE_MAX);
|
||||
});
|
||||
|
||||
it('maps eraser size back to the inverse slider position', () => {
|
||||
const { max, min } = appConfig.toolbar.eraser;
|
||||
const quarterRangeSize = min + (max - min) * 0.25;
|
||||
const quarterRangeSize = ERASER_SIZE_MIN + (ERASER_SIZE_MAX - ERASER_SIZE_MIN) * 0.25;
|
||||
|
||||
expect(getEraserSliderRatioFromSize(min)).toBe(0);
|
||||
expect(getEraserSliderRatioFromSize(ERASER_SIZE_MIN)).toBe(0);
|
||||
expect(getEraserSliderRatioFromSize(quarterRangeSize)).toBe(0.5);
|
||||
expect(getEraserSliderRatioFromSize(max)).toBe(1);
|
||||
expect(getEraserSliderRatioFromSize(ERASER_SIZE_MAX)).toBe(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
import { appConfig } from '../config';
|
||||
import type GameLoop from '../game-loop/game-loop';
|
||||
import { settings } from '../settings';
|
||||
import { DEFAULT_ERASER_SIZE, settings } from '../settings';
|
||||
import { queryRequiredElement } from '../utils/dom';
|
||||
|
||||
export const ERASER_SIZE_MIN = 24;
|
||||
export const ERASER_SIZE_MAX = 480;
|
||||
|
||||
const ERASER_CONTROL_SCALE_MIN = 0.74;
|
||||
const ERASER_CONTROL_SCALE_MAX = 1.34;
|
||||
|
||||
const clampEraserSize = (value: number): number => {
|
||||
const { default: defaultSize, max, min } = appConfig.toolbar.eraser;
|
||||
const safeValue = Number.isFinite(value) ? value : defaultSize;
|
||||
return Math.min(max, Math.max(min, Math.round(safeValue)));
|
||||
const safeValue = Number.isFinite(value) ? value : DEFAULT_ERASER_SIZE;
|
||||
return Math.min(ERASER_SIZE_MAX, Math.max(ERASER_SIZE_MIN, Math.round(safeValue)));
|
||||
};
|
||||
|
||||
const ERASER_SLIDER_MIN = 0;
|
||||
|
|
@ -19,13 +23,14 @@ const clampSliderRatio = (value: number): number => {
|
|||
};
|
||||
|
||||
const getEraserSizeRatio = (size: number): number => {
|
||||
const { max, min } = appConfig.toolbar.eraser;
|
||||
return (clampEraserSize(size) - min) / (max - min);
|
||||
return (clampEraserSize(size) - ERASER_SIZE_MIN) / (ERASER_SIZE_MAX - ERASER_SIZE_MIN);
|
||||
};
|
||||
|
||||
export const getEraserSizeFromSliderRatio = (sliderRatio: number): number => {
|
||||
const { max, min } = appConfig.toolbar.eraser;
|
||||
return clampEraserSize(min + (max - min) * clampSliderRatio(sliderRatio) ** 2);
|
||||
return clampEraserSize(
|
||||
ERASER_SIZE_MIN +
|
||||
(ERASER_SIZE_MAX - ERASER_SIZE_MIN) * clampSliderRatio(sliderRatio) ** 2
|
||||
);
|
||||
};
|
||||
|
||||
export const getEraserSliderRatioFromSize = (size: number): number =>
|
||||
|
|
@ -72,10 +77,8 @@ export class EraserSizeControl {
|
|||
|
||||
const sizeRatio = getEraserSizeRatio(size);
|
||||
const scale =
|
||||
appConfig.toolbar.eraser.controlScaleMin +
|
||||
(appConfig.toolbar.eraser.controlScaleMax -
|
||||
appConfig.toolbar.eraser.controlScaleMin) *
|
||||
sizeRatio;
|
||||
ERASER_CONTROL_SCALE_MIN +
|
||||
(ERASER_CONTROL_SCALE_MAX - ERASER_CONTROL_SCALE_MIN) * sizeRatio;
|
||||
this.control.style.setProperty('--eraser-progress', `${sliderRatio * 100}%`);
|
||||
this.control.style.setProperty('--eraser-control-scale', scale.toFixed(3));
|
||||
this.syncActiveState();
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { appConfig } from '../config';
|
||||
const DESKTOP_AUTO_HIDE_MEDIA_QUERY =
|
||||
'(min-width: 600px) and (hover: hover) and (pointer: fine)';
|
||||
const HIDE_DELAY_MS = 3000;
|
||||
const BOTTOM_REVEAL_DISTANCE_PX = 96;
|
||||
|
||||
export class MenuHider {
|
||||
private readonly desktopMediaQuery = window.matchMedia(
|
||||
appConfig.menuHider.desktopMediaQuery
|
||||
);
|
||||
private readonly desktopMediaQuery = window.matchMedia(DESKTOP_AUTO_HIDE_MEDIA_QUERY);
|
||||
private hideTimeout: number | undefined;
|
||||
private isHidden = false;
|
||||
private pointerInside = false;
|
||||
|
|
@ -95,7 +96,7 @@ export class MenuHider {
|
|||
if (this.canAutoHide) {
|
||||
this.hide();
|
||||
}
|
||||
}, appConfig.menuHider.hideDelayMs);
|
||||
}, HIDE_DELAY_MS);
|
||||
}
|
||||
|
||||
private reveal(): void {
|
||||
|
|
@ -134,6 +135,6 @@ export class MenuHider {
|
|||
|
||||
private isNearViewportBottom(clientY: number): boolean {
|
||||
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
|
||||
return clientY >= viewportHeight - appConfig.menuHider.bottomRevealDistancePx;
|
||||
return clientY >= viewportHeight - BOTTOM_REVEAL_DISTANCE_PX;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,48 @@
|
|||
import { appConfig } from '../config';
|
||||
import { settings } from '../settings';
|
||||
import {
|
||||
MAX_MIRROR_SEGMENT_COUNT,
|
||||
MIN_MIRROR_SEGMENT_COUNT,
|
||||
} from '../game-loop/stroke-mirroring';
|
||||
import { DEFAULT_MIRROR_SEGMENT_COUNT, settings } from '../settings';
|
||||
import { queryRequiredElement } from '../utils/dom';
|
||||
|
||||
const MIRROR_SEGMENT_STEP = 1;
|
||||
const MIRROR_SEGMENT_OFF_LABEL = 'Mirror off';
|
||||
const MIRROR_SEGMENT_FALLBACK_NAME = 'slices';
|
||||
const MIRROR_SEGMENT_NAMES = {
|
||||
2: 'halves',
|
||||
3: 'thirds',
|
||||
4: 'quarters',
|
||||
5: 'fifths',
|
||||
6: 'sixths',
|
||||
7: 'sevenths',
|
||||
8: 'eighths',
|
||||
9: 'ninths',
|
||||
10: 'tenths',
|
||||
11: 'elevenths',
|
||||
12: 'twelfths',
|
||||
} satisfies Record<number, string>;
|
||||
|
||||
const clampMirrorSegmentCount = (value: number): number => {
|
||||
const { default: defaultCount, max, min } = appConfig.toolbar.mirror;
|
||||
const safeValue = Number.isFinite(value) ? value : defaultCount;
|
||||
return Math.min(max, Math.max(min, Math.round(safeValue)));
|
||||
const safeValue = Number.isFinite(value) ? value : DEFAULT_MIRROR_SEGMENT_COUNT;
|
||||
return Math.min(
|
||||
MAX_MIRROR_SEGMENT_COUNT,
|
||||
Math.max(MIN_MIRROR_SEGMENT_COUNT, Math.round(safeValue))
|
||||
);
|
||||
};
|
||||
|
||||
const getMirrorSegmentRatio = (count: number): number => {
|
||||
const { max, min } = appConfig.toolbar.mirror;
|
||||
return (count - min) / (max - min);
|
||||
return (
|
||||
(count - MIN_MIRROR_SEGMENT_COUNT) /
|
||||
(MAX_MIRROR_SEGMENT_COUNT - MIN_MIRROR_SEGMENT_COUNT)
|
||||
);
|
||||
};
|
||||
|
||||
const formatMirrorSegmentCount = (count: number): string =>
|
||||
count <= 1
|
||||
? appConfig.toolbar.mirror.offLabel
|
||||
? MIRROR_SEGMENT_OFF_LABEL
|
||||
: `${count} ${
|
||||
appConfig.toolbar.mirror.names[
|
||||
count as keyof typeof appConfig.toolbar.mirror.names
|
||||
] ?? appConfig.toolbar.mirror.fallbackSegmentName
|
||||
MIRROR_SEGMENT_NAMES[count as keyof typeof MIRROR_SEGMENT_NAMES] ??
|
||||
MIRROR_SEGMENT_FALLBACK_NAME
|
||||
}`;
|
||||
|
||||
interface MirrorSegmentControlOptions {
|
||||
|
|
@ -50,9 +73,9 @@ export class MirrorSegmentControl {
|
|||
settings.mirrorSegmentCount = count;
|
||||
}
|
||||
|
||||
this.slider.min = appConfig.toolbar.mirror.min.toString();
|
||||
this.slider.max = appConfig.toolbar.mirror.max.toString();
|
||||
this.slider.step = appConfig.toolbar.mirror.step.toString();
|
||||
this.slider.min = MIN_MIRROR_SEGMENT_COUNT.toString();
|
||||
this.slider.max = MAX_MIRROR_SEGMENT_COUNT.toString();
|
||||
this.slider.step = MIRROR_SEGMENT_STEP.toString();
|
||||
this.slider.value = count.toString();
|
||||
|
||||
const label = formatMirrorSegmentCount(count);
|
||||
|
|
|
|||
Loading…
Reference in a new issue