Small improvements
This commit is contained in:
parent
a7c04b2bd8
commit
05c8a39bd8
19 changed files with 119 additions and 95 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,3 +1,5 @@
|
|||
node_modules
|
||||
dist
|
||||
test-results
|
||||
.DS_Store
|
||||
*.log
|
||||
|
|
|
|||
|
|
@ -6,5 +6,5 @@
|
|||
"endOfLine": "lf",
|
||||
"plugins": ["@ianvs/prettier-plugin-sort-imports"],
|
||||
"importOrder": ["<BUILTIN_MODULES>", "<THIRD_PARTY_MODULES>", "", "^[./]"],
|
||||
"importOrderTypeScriptVersion": "5.0.0"
|
||||
"importOrderTypeScriptVersion": "5.6.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ test('starts the WebGPU garden and accepts drawing input', async ({ page }) => {
|
|||
const startButton = page.getByRole('button', { name: 'Start' });
|
||||
await expect(startButton).toBeVisible();
|
||||
await expect(startButton).toBeEnabled({ timeout: 30_000 });
|
||||
await startButton.click();
|
||||
await page.keyboard.press('Enter');
|
||||
await expect(page.locator('body')).not.toHaveClass(/is-loading/, {
|
||||
timeout: 30_000,
|
||||
});
|
||||
|
|
@ -146,7 +146,7 @@ test('syncs the selected vibe with the URI', async ({ page }) => {
|
|||
|
||||
await expect(page).toHaveURL(/vibe=bone-archive/);
|
||||
|
||||
await page.locator('.next-vibe').click();
|
||||
await page.getByRole('button', { name: 'Next vibe' }).click();
|
||||
await expect(page).toHaveURL(/vibe=pelagic-caustics/);
|
||||
|
||||
await page.goBack();
|
||||
|
|
@ -160,8 +160,8 @@ test('keeps audio focus outlines scoped to the active control', async ({ page })
|
|||
await expect(page.locator('body')).not.toHaveClass(/is-loading/);
|
||||
|
||||
const audioControl = page.locator('.audio-control');
|
||||
const soundButton = page.locator('button.sound');
|
||||
const volumeSlider = page.locator('.volume-slider');
|
||||
const soundButton = page.getByRole('button', { name: /audio/i });
|
||||
const volumeSlider = page.getByRole('slider', { name: 'Master volume' });
|
||||
|
||||
await soundButton.click();
|
||||
await expect(audioControl).toHaveCSS('outline-style', 'none');
|
||||
|
|
@ -201,7 +201,7 @@ test('keeps the config overlay scrollable and dismissible on mobile', async ({
|
|||
timeout: 30_000,
|
||||
});
|
||||
|
||||
const settingsButton = page.locator('button.settings');
|
||||
const settingsButton = page.getByRole('button', { name: 'Show config overlay' });
|
||||
await settingsButton.click();
|
||||
|
||||
const pane = page.locator('.config-pane');
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
"generate-icons": "pwa-assets-generator"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
"node": ">=22"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { DEFAULT_AUDIO_VOLUME } from '../consts';
|
||||
import type { PianoNoteRole } from './garden-audio-types';
|
||||
|
||||
const DEFAULT_AUDIO_VOLUME = 0.5;
|
||||
|
||||
type GardenAudioChordQuality = 'major' | 'minor' | 'sus2' | 'sus4';
|
||||
|
||||
export interface GardenAudioChord {
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ const graphTuning = {
|
|||
eventBusGain: 1,
|
||||
noiseMax: 1,
|
||||
noiseMin: -1,
|
||||
latencyHint: 'interactive' as AudioContextLatencyCategory,
|
||||
outputFilterType: 'highpass' as BiquadFilterType,
|
||||
latencyHint: 'interactive',
|
||||
outputFilterType: 'highpass',
|
||||
compressor: {
|
||||
thresholdDb: -18,
|
||||
kneeDb: 18,
|
||||
|
|
@ -32,7 +32,7 @@ const graphTuning = {
|
|||
attackSeconds: 0.018,
|
||||
releaseSeconds: 0.18,
|
||||
},
|
||||
};
|
||||
} as const;
|
||||
const delayFilterTuning = {
|
||||
feedbackHighPassHz: 180,
|
||||
feedbackLowPassHz: 5200,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import { PIANO_SCHEDULE_AHEAD_SECONDS } from './piano-sampler';
|
|||
|
||||
const GENERATIVE_LOOKAHEAD_SECONDS = 0.3;
|
||||
const GENERATIVE_START_DELAY_SECONDS = 0.02;
|
||||
const TEXTURE_ONSET_EXPRESSION = 0.15;
|
||||
const SUPPORT_ONSET_EXPRESSION = 0.4;
|
||||
|
||||
const chordVoicings: Record<
|
||||
GardenAudioChord['quality'],
|
||||
|
|
@ -1087,6 +1089,9 @@ export class GenerativePianoEngine {
|
|||
}
|
||||
|
||||
private shouldPlaySupport(expression: number, barIndex: number): boolean {
|
||||
if (expression < SUPPORT_ONSET_EXPRESSION) {
|
||||
return false;
|
||||
}
|
||||
if (expression >= generativePianoTuning.supportNote.expressionThreshold) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1098,19 +1103,17 @@ export class GenerativePianoEngine {
|
|||
}
|
||||
|
||||
private shouldPlayTexture(expression: number, barIndex: number): boolean {
|
||||
if (expression < TEXTURE_ONSET_EXPRESSION) {
|
||||
return false;
|
||||
}
|
||||
if (expression >= generativePianoTuning.textureNote.mediumExpressionThreshold) {
|
||||
return barIndex % generativePianoTuning.textureNote.intenseSpacing === 0;
|
||||
}
|
||||
const spacing =
|
||||
expression < generativePianoTuning.textureNote.idleExpressionThreshold
|
||||
? generativePianoTuning.idleTextureBarSpacing
|
||||
: expression < generativePianoTuning.textureNote.mediumExpressionThreshold
|
||||
? generativePianoTuning.mediumTextureBarSpacing
|
||||
: generativePianoTuning.textureNote.intenseSpacing;
|
||||
|
||||
return (
|
||||
barIndex % spacing ===
|
||||
(spacing === generativePianoTuning.textureNote.intenseSpacing
|
||||
? 0
|
||||
: generativePianoTuning.textureNote.idlePhase)
|
||||
);
|
||||
: generativePianoTuning.mediumTextureBarSpacing;
|
||||
return barIndex % spacing === generativePianoTuning.textureNote.idlePhase % spacing;
|
||||
}
|
||||
|
||||
private getSupportOffsets(
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ const noiseBurstTuning = {
|
|||
offsetRandomSeconds: 0.4,
|
||||
scheduleAheadSeconds: 0.002,
|
||||
silentGain: 0.0001,
|
||||
filterType: 'bandpass' as BiquadFilterType,
|
||||
};
|
||||
filterType: 'bandpass',
|
||||
} as const;
|
||||
|
||||
export class NoiseBurstPlayer {
|
||||
public constructor(private readonly graph: GardenAudioGraph) {}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ interface ActivePianoVoice {
|
|||
}
|
||||
|
||||
const pianoSamplerTuning = {
|
||||
filterType: 'lowpass' as BiquadFilterType,
|
||||
filterType: 'lowpass',
|
||||
filterQ: 0.7,
|
||||
minDurationSeconds: 0.08,
|
||||
minFadeSeconds: 0.08,
|
||||
|
|
@ -22,7 +22,7 @@ const pianoSamplerTuning = {
|
|||
tailStopExtraSeconds: 0.05,
|
||||
voiceStealFadeSeconds: 0.025,
|
||||
voiceStealStopSeconds: 0.05,
|
||||
};
|
||||
} as const;
|
||||
|
||||
export class PianoSampler {
|
||||
private samples: Array<LoadedPianoSample> = [];
|
||||
|
|
@ -178,7 +178,11 @@ export class PianoSampler {
|
|||
|
||||
this.trimActiveVoices(scheduledStart);
|
||||
while (this.activeVoices.length >= this.config.piano.maxVoices) {
|
||||
this.stopVoice(this.activeVoices.shift() as ActivePianoVoice, scheduledStart);
|
||||
const oldest = this.activeVoices.shift();
|
||||
if (!oldest) {
|
||||
break;
|
||||
}
|
||||
this.stopVoice(oldest, scheduledStart);
|
||||
}
|
||||
|
||||
filter.type = pianoSamplerTuning.filterType;
|
||||
|
|
@ -220,11 +224,8 @@ export class PianoSampler {
|
|||
);
|
||||
}
|
||||
|
||||
private computeNoteGain(velocity: number, scale = 1): number {
|
||||
return Math.max(
|
||||
pianoSamplerTuning.minGain,
|
||||
this.config.piano.gain * velocity * scale
|
||||
);
|
||||
private computeNoteGain(velocity: number): number {
|
||||
return Math.max(pianoSamplerTuning.minGain, this.config.piano.gain * velocity);
|
||||
}
|
||||
|
||||
private findNearestSample(midi: number): LoadedPianoSample | null {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import { defaultSettings } from './config/default-settings';
|
|||
import { runtimeControls } from './config/runtime-controls';
|
||||
import type { GardenAppConfig } from './config/types';
|
||||
import { defaultVibeId, vibePresets } from './config/vibe-presets';
|
||||
import { APP_STORAGE_KEYS, DEFAULT_AUDIO_VOLUME } from './consts';
|
||||
|
||||
const DEFAULT_AUDIO_VOLUME = 0.5;
|
||||
|
||||
export {
|
||||
normalizeNumberControlValue,
|
||||
|
|
@ -114,9 +115,9 @@ export const appConfig = {
|
|||
},
|
||||
},
|
||||
storage: {
|
||||
audioMutedKey: APP_STORAGE_KEYS.audioMuted,
|
||||
audioVolumeKey: APP_STORAGE_KEYS.audioVolume,
|
||||
vibeKey: APP_STORAGE_KEYS.vibe,
|
||||
audioMutedKey: 'fleeting-garden:audio-muted',
|
||||
audioVolumeKey: 'fleeting-garden:audio-volume',
|
||||
vibeKey: 'fleeting-garden:vibe',
|
||||
},
|
||||
toolbar: {
|
||||
eraser: {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,19 @@ const FINAL_VIBE_NAMES = [
|
|||
'Chrome Pollen',
|
||||
];
|
||||
|
||||
const BLENDED_BRUSH_SIZE_MIN = 17;
|
||||
const BLENDED_CLARITY_MAX = 0.56;
|
||||
const SOFT_PARTICLE_BRUSH_SIZE_MAX = 5;
|
||||
const SOFT_PARTICLE_CLARITY_MAX = 0.2;
|
||||
|
||||
// Performance guardrails — bumping any of these is an explicit perf trade-off.
|
||||
const MAX_SPAWN_PER_PIXEL = 0.38;
|
||||
const MAX_BRUSH_SIZE = 36;
|
||||
const HIGH_DENSITY_SPAWN_THRESHOLD = 0.28;
|
||||
const HIGH_DENSITY_DECAY_LIMIT = 940;
|
||||
const HIGH_DENSITY_BRUSH_SIZE_LIMIT = 14;
|
||||
const HIGH_DENSITY_TRAIL_WEIGHT_LIMIT = 0.055;
|
||||
|
||||
describe('vibePresets', () => {
|
||||
it('keeps the classic preset set distinct', () => {
|
||||
expect(vibePresets.map((preset) => preset.name)).toEqual(FINAL_VIBE_NAMES);
|
||||
|
|
@ -22,12 +35,16 @@ describe('vibePresets', () => {
|
|||
it('includes both blended and visibly particulate styles', () => {
|
||||
const blendedNames = vibePresets
|
||||
.filter(
|
||||
(preset) => preset.settings.brushSize >= 17 && preset.settings.clarity <= 0.56
|
||||
(preset) =>
|
||||
preset.settings.brushSize >= BLENDED_BRUSH_SIZE_MIN &&
|
||||
preset.settings.clarity <= BLENDED_CLARITY_MAX
|
||||
)
|
||||
.map((preset) => preset.name);
|
||||
const softParticleNames = vibePresets
|
||||
.filter(
|
||||
(preset) => preset.settings.brushSize <= 5 && preset.settings.clarity <= 0.2
|
||||
(preset) =>
|
||||
preset.settings.brushSize <= SOFT_PARTICLE_BRUSH_SIZE_MAX &&
|
||||
preset.settings.clarity <= SOFT_PARTICLE_CLARITY_MAX
|
||||
)
|
||||
.map((preset) => preset.name);
|
||||
|
||||
|
|
@ -40,17 +57,17 @@ describe('vibePresets', () => {
|
|||
const { name, settings } = preset;
|
||||
const presetViolations: Array<string> = [];
|
||||
|
||||
if (settings.spawnPerPixel > 0.38) {
|
||||
presetViolations.push(`${name} density exceeds 0.38`);
|
||||
if (settings.spawnPerPixel > MAX_SPAWN_PER_PIXEL) {
|
||||
presetViolations.push(`${name} density exceeds ${MAX_SPAWN_PER_PIXEL}`);
|
||||
}
|
||||
if (settings.brushSize > 36) {
|
||||
presetViolations.push(`${name} brush size exceeds 36`);
|
||||
if (settings.brushSize > MAX_BRUSH_SIZE) {
|
||||
presetViolations.push(`${name} brush size exceeds ${MAX_BRUSH_SIZE}`);
|
||||
}
|
||||
if (
|
||||
settings.spawnPerPixel >= 0.28 &&
|
||||
(settings.decayRateTrails > 940 ||
|
||||
settings.brushSize > 14 ||
|
||||
settings.individualTrailWeight > 0.055)
|
||||
settings.spawnPerPixel >= HIGH_DENSITY_SPAWN_THRESHOLD &&
|
||||
(settings.decayRateTrails > HIGH_DENSITY_DECAY_LIMIT ||
|
||||
settings.brushSize > HIGH_DENSITY_BRUSH_SIZE_LIMIT ||
|
||||
settings.individualTrailWeight > HIGH_DENSITY_TRAIL_WEIGHT_LIMIT)
|
||||
) {
|
||||
presetViolations.push(`${name} combines high density with too much persistence`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -155,20 +155,20 @@ export const vibePresets: Array<VibePreset> = [
|
|||
moveSpeed: 270,
|
||||
sensorOffsetAngle: 36,
|
||||
sensorOffsetDistance: 51,
|
||||
spawnPerPixel: 0.13999999999999999,
|
||||
strokeAngleJitterRadians: 0.44999999999999996,
|
||||
spawnPerPixel: 0.14,
|
||||
strokeAngleJitterRadians: 0.45,
|
||||
turnSpeed: 22,
|
||||
turnWhenLost: 6.071532165918825e-17,
|
||||
turnWhenLost: 0,
|
||||
},
|
||||
audio: {
|
||||
...defaultGardenAudioVibeSettings,
|
||||
idleIntensity: 0.12,
|
||||
bpm: 60,
|
||||
rampUpIntensity: 0.7000000000000001,
|
||||
rampUpIntensity: 0.7,
|
||||
rampUpTime: 0.14,
|
||||
noteLength: 0.86,
|
||||
notePitchOffset: -2,
|
||||
brightness: 0.8400000000000001,
|
||||
brightness: 0.84,
|
||||
scale: musicScales.lydian,
|
||||
progression: musicProgressions.aurora,
|
||||
},
|
||||
|
|
@ -188,23 +188,23 @@ export const vibePresets: Array<VibePreset> = [
|
|||
brushSize: 9.75,
|
||||
clarity: 0.437,
|
||||
decayRateTrails: 915,
|
||||
forwardRotationScale: 2.0816681711721685e-17,
|
||||
forwardRotationScale: 0,
|
||||
individualTrailWeight: 0.1,
|
||||
moveSpeed: 216,
|
||||
sensorOffsetAngle: 24,
|
||||
sensorOffsetDistance: 17,
|
||||
spawnPerPixel: 0.24,
|
||||
strokeAngleJitterRadians: 0.16999999999999993,
|
||||
strokeAngleJitterRadians: 0.17,
|
||||
turnSpeed: 33,
|
||||
turnWhenLost: 0.42000000000000004,
|
||||
turnWhenLost: 0.42,
|
||||
},
|
||||
audio: {
|
||||
...defaultGardenAudioVibeSettings,
|
||||
idleIntensity: 0.55,
|
||||
bpm: 72,
|
||||
rampUpIntensity: 1.42,
|
||||
rampUpTime: 0.07000000000000002,
|
||||
noteLength: 0.7000000000000001,
|
||||
rampUpTime: 0.07,
|
||||
noteLength: 0.7,
|
||||
notePitchOffset: 0,
|
||||
brightness: 0.94,
|
||||
scale: musicScales.naturalMinor,
|
||||
|
|
@ -227,21 +227,21 @@ export const vibePresets: Array<VibePreset> = [
|
|||
clarity: 0.74,
|
||||
decayRateTrails: 962,
|
||||
forwardRotationScale: 0.3,
|
||||
individualTrailWeight: 0.052000000000000005,
|
||||
individualTrailWeight: 0.052,
|
||||
moveSpeed: 72,
|
||||
sensorOffsetAngle: 42,
|
||||
sensorOffsetDistance: 54,
|
||||
spawnPerPixel: 0.15999999999999998,
|
||||
strokeAngleJitterRadians: 3.1399999999999997,
|
||||
spawnPerPixel: 0.16,
|
||||
strokeAngleJitterRadians: 3.14,
|
||||
turnSpeed: 44,
|
||||
turnWhenLost: 0.9200000000000002,
|
||||
turnWhenLost: 0.92,
|
||||
},
|
||||
audio: {
|
||||
...defaultGardenAudioVibeSettings,
|
||||
idleIntensity: 0.13,
|
||||
bpm: 68,
|
||||
rampUpIntensity: 1.46,
|
||||
rampUpTime: 0.10000000000000002,
|
||||
rampUpTime: 0.1,
|
||||
noteLength: 0.6,
|
||||
notePitchOffset: -3,
|
||||
brightness: 1.21,
|
||||
|
|
@ -307,7 +307,7 @@ export const vibePresets: Array<VibePreset> = [
|
|||
moveSpeed: 28,
|
||||
sensorOffsetAngle: 34,
|
||||
sensorOffsetDistance: 66,
|
||||
spawnPerPixel: 0.05499999999999998,
|
||||
spawnPerPixel: 0.055,
|
||||
strokeAngleJitterRadians: 0,
|
||||
turnSpeed: 30,
|
||||
turnWhenLost: 1.52,
|
||||
|
|
@ -317,7 +317,7 @@ export const vibePresets: Array<VibePreset> = [
|
|||
idleIntensity: 0.33,
|
||||
bpm: 127,
|
||||
rampUpIntensity: 0.66,
|
||||
rampUpTime: 0.03000000000000001,
|
||||
rampUpTime: 0.03,
|
||||
noteLength: 0.92,
|
||||
notePitchOffset: 10,
|
||||
brightness: 1.42,
|
||||
|
|
@ -341,7 +341,7 @@ export const vibePresets: Array<VibePreset> = [
|
|||
clarity: 0.1,
|
||||
decayRateTrails: 922,
|
||||
forwardRotationScale: 0.5,
|
||||
individualTrailWeight: 0.026000000000000002,
|
||||
individualTrailWeight: 0.026,
|
||||
moveSpeed: 86,
|
||||
sensorOffsetAngle: 46,
|
||||
sensorOffsetDistance: 14,
|
||||
|
|
@ -355,7 +355,7 @@ export const vibePresets: Array<VibePreset> = [
|
|||
idleIntensity: 0.11,
|
||||
bpm: 150,
|
||||
rampUpIntensity: 2,
|
||||
rampUpTime: 0.06000000000000001,
|
||||
rampUpTime: 0.06,
|
||||
noteLength: 1.8,
|
||||
notePitchOffset: -12,
|
||||
brightness: 0.5,
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
export const ENABLED_FLAG_VALUE = '1';
|
||||
export const DISABLED_FLAG_VALUE = '0';
|
||||
|
||||
export const DEFAULT_AUDIO_VOLUME = 0.5;
|
||||
|
||||
export const APP_STORAGE_KEYS = {
|
||||
audioMuted: 'fleeting-garden:audio-muted',
|
||||
audioVolume: 'fleeting-garden:audio-volume',
|
||||
vibe: 'fleeting-garden:vibe',
|
||||
} as const;
|
||||
|
|
@ -1,19 +1,18 @@
|
|||
import { settings } from '../settings';
|
||||
|
||||
export class FramePerformance {
|
||||
private readonly adaptiveRefreshTargetFps = 60;
|
||||
private readonly initialFps = this.adaptiveRefreshTargetFps;
|
||||
public smoothedFps = this.initialFps;
|
||||
const ADAPTIVE_REFRESH_TARGET_FPS = 60;
|
||||
const ADAPTIVE_CAP_DECREASE_AGENTS_PER_SECOND = 200_000;
|
||||
const FRAME_GAP_RESET_SECONDS = 1;
|
||||
const FPS_HEADROOM = 0.9;
|
||||
const FPS_SMOOTHING_NEW = 0.06;
|
||||
const FPS_SMOOTHING_RETAIN = 1 - FPS_SMOOTHING_NEW;
|
||||
|
||||
export class FramePerformance {
|
||||
public smoothedFps = ADAPTIVE_REFRESH_TARGET_FPS;
|
||||
public measuredFps = 0;
|
||||
public frameDeltaSeconds = 0;
|
||||
public measuredFrameTimeMs = 0;
|
||||
|
||||
private readonly adaptiveCapDecreaseAgentsPerSecond = 200_000;
|
||||
private readonly frameGapResetSeconds = 1;
|
||||
private readonly fpsHeadroom = 0.9;
|
||||
private readonly fpsSmoothingNew = 0.06;
|
||||
private readonly fpsSmoothingRetain = 1 - this.fpsSmoothingNew;
|
||||
private previousFrameTime: DOMHighResTimeStamp | null = null;
|
||||
|
||||
public get adaptiveCapInitial(): number {
|
||||
|
|
@ -25,13 +24,13 @@ export class FramePerformance {
|
|||
}
|
||||
|
||||
public get hasAdaptiveCapHeadroom(): boolean {
|
||||
return this.smoothedFps >= this.adaptiveRefreshTargetFps * this.fpsHeadroom;
|
||||
return this.smoothedFps >= ADAPTIVE_REFRESH_TARGET_FPS * FPS_HEADROOM;
|
||||
}
|
||||
|
||||
public get adaptiveCapDecreaseAgents(): number {
|
||||
return Math.max(
|
||||
1,
|
||||
Math.ceil(this.adaptiveCapDecreaseAgentsPerSecond * this.frameDeltaSeconds)
|
||||
Math.ceil(ADAPTIVE_CAP_DECREASE_AGENTS_PER_SECOND * this.frameDeltaSeconds)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -51,11 +50,10 @@ export class FramePerformance {
|
|||
this.frameDeltaSeconds = deltaSeconds;
|
||||
this.measuredFrameTimeMs = deltaSeconds * 1000;
|
||||
this.measuredFps = fps;
|
||||
if (deltaSeconds > this.frameGapResetSeconds) {
|
||||
if (deltaSeconds > FRAME_GAP_RESET_SECONDS) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.smoothedFps =
|
||||
this.smoothedFps * this.fpsSmoothingRetain + fps * this.fpsSmoothingNew;
|
||||
this.smoothedFps = this.smoothedFps * FPS_SMOOTHING_RETAIN + fps * FPS_SMOOTHING_NEW;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,8 +14,7 @@ export class SimulationTextures {
|
|||
public trailMapA: ResizableTexture;
|
||||
public trailMapB: ResizableTexture;
|
||||
// Per-frame deposit accumulator: cleared each frame, written sparsely by
|
||||
// agents, then read by diffuse alongside trailMapA. Replaces the previous
|
||||
// full-resolution copyTrailMapAToB seed.
|
||||
// agents, then read by diffuse alongside trailMapA.
|
||||
public readonly depositMap: ResizableTexture;
|
||||
public readonly eraserMask: ResizableTexture;
|
||||
public sourceMapA: ResizableTexture;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { appConfig } from '../config';
|
||||
import { DISABLED_FLAG_VALUE, ENABLED_FLAG_VALUE } from '../consts';
|
||||
import type GameLoop from '../game-loop/game-loop';
|
||||
import { readBrowserStorage, writeBrowserStorage } from '../utils/browser-storage';
|
||||
import { queryRequiredElement } from '../utils/dom';
|
||||
|
|
@ -21,6 +20,9 @@ const readInitialAudioVolume = (): number => {
|
|||
const formatStoredAudioVolume = (volume: number): string =>
|
||||
clampAudioVolume(volume).toFixed(2);
|
||||
|
||||
const STORED_MUTED_TRUE = '1';
|
||||
const STORED_MUTED_FALSE = '0';
|
||||
|
||||
interface AudioControlOptions {
|
||||
getGame: () => GameLoop | null;
|
||||
hasStarted: () => boolean;
|
||||
|
|
@ -43,7 +45,7 @@ export class AudioControl {
|
|||
|
||||
private audioVolume = readInitialAudioVolume();
|
||||
private isMutedState =
|
||||
readBrowserStorage(appConfig.storage.audioMutedKey) === ENABLED_FLAG_VALUE ||
|
||||
readBrowserStorage(appConfig.storage.audioMutedKey) === STORED_MUTED_TRUE ||
|
||||
this.audioVolume <= 0;
|
||||
|
||||
public constructor(private readonly options: AudioControlOptions) {
|
||||
|
|
@ -140,7 +142,7 @@ export class AudioControl {
|
|||
private persist(): void {
|
||||
writeBrowserStorage(
|
||||
appConfig.storage.audioMutedKey,
|
||||
this.isMutedState ? ENABLED_FLAG_VALUE : DISABLED_FLAG_VALUE
|
||||
this.isMutedState ? STORED_MUTED_TRUE : STORED_MUTED_FALSE
|
||||
);
|
||||
writeBrowserStorage(
|
||||
appConfig.storage.audioVolumeKey,
|
||||
|
|
|
|||
|
|
@ -30,13 +30,24 @@ export class SplashScreen {
|
|||
public awaitStart(onStart: () => void): Promise<void> {
|
||||
this.startButton.disabled = false;
|
||||
return new Promise<void>((resolve) => {
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key !== 'Enter' || event.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
this.startButton.click();
|
||||
};
|
||||
const onClick = () => {
|
||||
this.startButton.removeEventListener('click', onClick);
|
||||
document.removeEventListener('keydown', onKeyDown);
|
||||
onStart();
|
||||
this.setVisible(this.splash, false);
|
||||
resolve();
|
||||
};
|
||||
|
||||
this.startButton.addEventListener('click', onClick);
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
:root {
|
||||
--transition-time: 200ms;
|
||||
--transition-time-long: 350ms;
|
||||
--accent-color: rgb(255, 93, 162);
|
||||
--accent-color: rgb(255 93 162);
|
||||
--main-color: #aaa;
|
||||
--normal-margin: 2rem;
|
||||
--small-margin: 1rem;
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ export default defineConfig(({ command }) => ({
|
|||
},
|
||||
server: {
|
||||
host: true,
|
||||
hmr: false,
|
||||
},
|
||||
test: {
|
||||
environment: 'node',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue