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