This commit is contained in:
Andras Schmelczer 2026-05-13 21:07:10 +01:00
parent 34ac200437
commit 39b0160064
136 changed files with 7144 additions and 1965 deletions

915
src/config.ts Normal file
View file

@ -0,0 +1,915 @@
import type {
GardenAudioChord,
GardenAudioConfig,
GardenAudioVibeProfile,
} from './audio/garden-audio-config';
import type { GameLoopSettings } from './game-loop/game-loop-settings';
import type { AgentSettings } from './pipelines/agents/agent-settings';
import type { BrushSettings } from './pipelines/brush/brush-settings';
import type { DiffusionSettings } from './pipelines/diffusion/diffusion-settings';
import type { RenderSettings } from './pipelines/render/render-settings';
export type GardenRuntimeSettings = GameLoopSettings &
AgentSettings &
BrushSettings &
DiffusionSettings &
RenderSettings;
export type GardenVibeSettings = Partial<
Pick<
GardenRuntimeSettings,
| 'agentBudgetMax'
| 'brushSize'
| 'clarity'
| 'decayRateTrails'
| 'diffusionRateTrails'
| 'individualTrailWeight'
| 'moveSpeed'
| 'sensorOffsetAngle'
| 'sensorOffsetDistance'
| 'spawnPerPixel'
| 'turnSpeed'
>
>;
export interface VibePreset {
id: string;
name: string;
colors: [string, string, string];
backgroundColor: string;
settings: GardenVibeSettings;
audio: GardenAudioVibeProfile;
}
export interface NumberControlConfig {
folder: string;
integer?: boolean;
label?: string;
max: number;
min: number;
step?: number;
}
export type RuntimeSettingControlConfig = {
[Key in keyof GardenRuntimeSettings]: NumberControlConfig;
};
export interface GardenAppConfig {
audio: GardenAudioConfig;
audioEngine: {
energy: {
attackSeconds: number;
decaySeconds: number;
releaseSeconds: number;
strokeDecaySeconds: number;
};
eraser: {
canvasWidthRatioForFullSize: number;
defaultSizePixels: number;
durationSeconds: number;
filterPressureWeight: number;
filterSizeWeight: number;
filterSpeedWeight: number;
gainBase: number;
gainPressureWeight: number;
gainSizeWeight: number;
gainSpeedWeight: number;
};
delay: {
erasingActivity: number;
};
gestureFadeSeconds: number;
graph: {
closeGain: number;
closeRampSeconds: number;
delayActivityFeedbackWeight: number;
delayFeedbackMax: number;
delayFeedbackMin: number;
delayOutputActivityWeight: number;
delayOutputBase: number;
delayTimeRampSeconds: number;
eventBusGain: number;
noiseMax: number;
noiseMin: number;
unlockBufferLength: number;
unlockSampleRate: number;
};
input: {
distanceEnergyBase: number;
distanceEnergyScale: number;
distanceForFullEnergyPixels: number;
fallbackFrameSeconds: number;
penMinPressure: number;
strokeEnergyBase: number;
strokeEnergyPressureWeight: number;
strokeEnergySpeedWeight: number;
};
muteGain: number;
muteRampSeconds: number;
noiseBurst: {
attackSeconds: number;
filterQ: number;
offsetRandomSeconds: number;
scheduleAheadSeconds: number;
silentGain: number;
};
piano: {
fadeStopExtraSeconds: number;
defaultFadeSeconds: number;
fadeTimeConstantRatio: number;
filterQ: number;
gainAttackSeconds: number;
lowpassMaxHz: number;
lowpassMinHz: number;
minDurationSeconds: number;
minFadeSeconds: number;
minGain: number;
pitchSemitonesPerOctave: number;
scheduleAheadSeconds: number;
sustainBase: number;
sustainVelocityRange: number;
tailStopExtraSeconds: number;
voiceStealFadeSeconds: number;
voiceStealStopSeconds: number;
};
startDelaySeconds: number;
vibeChangeStingerMinIntervalSeconds: number;
};
deltaTime: {
fpsExponentialDecayStrength: number;
maxDeltaTimeSeconds: number;
minDeltaTimeSeconds: number;
};
export4k: {
bytesPerPixel: number;
height: number;
jsHeapSafetyMultiplier: number;
lowMemoryDeviceGiB: number;
lowMemoryExportFraction: number;
rowAlignmentBytes: number;
width: number;
};
menuHider: {
bottomRevealDistancePx: number;
intervalMs: number;
timeToLiveMs: number;
};
pipelines: {
brush: {
maxLineCount: number;
};
diffusion: {
minDiffusionRate: number;
};
eraser: {
maxSegmentCount: number;
maxTextureLineCount: number;
segmentFloatCount: number;
workgroupSize: number;
};
};
runtimeSettings: {
controls: RuntimeSettingControlConfig;
defaults: GardenRuntimeSettings;
};
simulation: {
budget: {
fpsHeadroom: number;
fpsSmoothingNew: number;
fpsSmoothingRetain: number;
initialTargetAgentBudget: number;
rampAgentsPerSecond: number;
refreshTargetDecay: number;
};
brushEffectFramesPerSecond: number;
globalAgentCap: number;
initialAgentCount: number;
intro: {
angleJitterRadians: number;
circleMaxSideRatio: number;
circleMinSideRatio: number;
drawHintClass: string;
drawHintDelayMs: number;
durationSeconds: number;
entryJitterSideRatio: number;
fontScaleDown: number;
initialFontHeightRatio: number;
initialFontWidthRatio: number;
letterSpacingEm: number;
maskAlphaThreshold: number;
maskGradientThreshold: number;
maskSampleDensity: number;
maxHeightRatio: number;
maxWidthRatio: number;
minEntryJitterPx: number;
minFontSizePx: number;
minTargetJitterPx: number;
radialJitterRatio: number;
targetDelayDistanceMultiplier: number;
targetDelayMax: number;
targetDelayRandomMultiplier: number;
targetJitterSideRatio: number;
title: string;
titleColorCutLetters: [number, number];
titleRadiusMultiplier: number;
titleStrokeWidthMinPx: number;
titleStrokeWidthRatio: number;
verticalAnchor: number;
};
introCameraZoom: number;
introMoveSpeedBaseMultiplier: number;
introMoveSpeedProgressMultiplier: number;
maxMirrorSegmentCount: number;
stroke: {
angleJitterRadians: number;
densityMultiplier: number;
maxAgentCount: number;
minAgentCount: number;
};
};
storage: {
audioMutedKey: string;
vibeKey: string;
};
telemetry: {
enabled: boolean;
intervalMs: number;
};
toolbar: {
eraser: {
controlScaleMax: number;
controlScaleMin: number;
default: number;
max: number;
min: number;
step: number;
};
mirror: {
default: number;
max: number;
min: number;
names: Record<number, string>;
step: number;
};
};
tuningPane: {
expandedDepth: number;
startHidden: boolean;
title: string;
};
vibes: {
defaultVibeId: string;
presets: Array<VibePreset>;
};
}
const majorProgression: Array<GardenAudioChord> = [
{ rootOffset: 0, quality: 'major' },
{ rootOffset: 9, quality: 'minor' },
{ rootOffset: 5, quality: 'major' },
{ rootOffset: 7, quality: 'major' },
];
const minorProgression: Array<GardenAudioChord> = [
{ rootOffset: 0, quality: 'minor' },
{ rootOffset: 8, quality: 'major' },
{ rootOffset: 3, quality: 'major' },
{ rootOffset: 10, quality: 'major' },
];
const majorPentatonic = [0, 2, 4, 7, 9];
const minorPentatonic = [0, 3, 5, 7, 10];
const defaultVibeId = 'candy-rain';
const vibePresets: Array<VibePreset> = [
{
id: 'candy-rain',
name: 'Candy Rain',
colors: ['#ff5da2', '#36d7d0', '#ffd84d'],
backgroundColor: '#10151f',
settings: {
agentBudgetMax: 1_000_000,
brushSize: 14,
clarity: 0.62,
decayRateTrails: 965,
diffusionRateTrails: 0.22,
individualTrailWeight: 0.07,
moveSpeed: 82,
sensorOffsetAngle: 34,
sensorOffsetDistance: 38,
spawnPerPixel: 0.22,
turnSpeed: 58,
},
audio: {
rootMidi: 57,
scale: majorPentatonic,
brightness: 1.04,
delayTimeMultiplier: 0.92,
progression: majorProgression,
},
},
{
id: 'sunlit-moss',
name: 'Sunlit Moss',
colors: ['#83d483', '#f6d76b', '#5ec1a1'],
backgroundColor: '#172016',
settings: {
agentBudgetMax: 900_000,
brushSize: 16,
clarity: 0.68,
decayRateTrails: 975,
diffusionRateTrails: 0.18,
individualTrailWeight: 0.06,
moveSpeed: 70,
sensorOffsetAngle: 28,
sensorOffsetDistance: 46,
spawnPerPixel: 0.18,
turnSpeed: 44,
},
audio: {
rootMidi: 53,
scale: majorPentatonic,
brightness: 0.92,
delayTimeMultiplier: 1.08,
progression: [
{ rootOffset: 0, quality: 'major' },
{ rootOffset: 7, quality: 'major' },
{ rootOffset: 9, quality: 'minor' },
{ rootOffset: 5, quality: 'major' },
],
},
},
{
id: 'coral-tide',
name: 'Coral Tide',
colors: ['#ff7f6e', '#40b8ff', '#f4f0a6'],
backgroundColor: '#0f1822',
settings: {
agentBudgetMax: 1_000_000,
brushSize: 13,
clarity: 0.58,
decayRateTrails: 955,
diffusionRateTrails: 0.28,
individualTrailWeight: 0.055,
moveSpeed: 90,
sensorOffsetAngle: 36,
sensorOffsetDistance: 35,
spawnPerPixel: 0.25,
turnSpeed: 62,
},
audio: {
rootMidi: 50,
scale: minorPentatonic,
brightness: 1,
delayTimeMultiplier: 1.12,
progression: minorProgression,
},
},
{
id: 'moon-orchid',
name: 'Moon Orchid',
colors: ['#c993ff', '#7dd8ff', '#f0f4ff'],
backgroundColor: '#14121d',
settings: {
agentBudgetMax: 850_000,
brushSize: 12,
clarity: 0.64,
decayRateTrails: 968,
diffusionRateTrails: 0.2,
individualTrailWeight: 0.065,
moveSpeed: 76,
sensorOffsetAngle: 32,
sensorOffsetDistance: 42,
spawnPerPixel: 0.2,
turnSpeed: 52,
},
audio: {
rootMidi: 49,
scale: minorPentatonic,
brightness: 0.9,
delayTimeMultiplier: 1.24,
progression: minorProgression,
},
},
{
id: 'peach-neon',
name: 'Peach Neon',
colors: ['#ff9b73', '#5bf0a9', '#6ea8ff'],
backgroundColor: '#191716',
settings: {
agentBudgetMax: 1_000_000,
brushSize: 15,
clarity: 0.55,
decayRateTrails: 948,
diffusionRateTrails: 0.32,
individualTrailWeight: 0.05,
moveSpeed: 96,
sensorOffsetAngle: 40,
sensorOffsetDistance: 32,
spawnPerPixel: 0.24,
turnSpeed: 70,
},
audio: {
rootMidi: 56,
scale: majorPentatonic,
brightness: 1.08,
delayTimeMultiplier: 0.86,
progression: majorProgression,
},
},
{
id: 'frost-bloom',
name: 'Frost Bloom',
colors: ['#b4f7ff', '#9ec8ff', '#ffb8d2'],
backgroundColor: '#101820',
settings: {
agentBudgetMax: 750_000,
brushSize: 18,
clarity: 0.7,
decayRateTrails: 982,
diffusionRateTrails: 0.14,
individualTrailWeight: 0.075,
moveSpeed: 62,
sensorOffsetAngle: 26,
sensorOffsetDistance: 52,
spawnPerPixel: 0.16,
turnSpeed: 40,
},
audio: {
rootMidi: 62,
scale: majorPentatonic,
brightness: 0.88,
delayTimeMultiplier: 1.32,
progression: [
{ rootOffset: 0, quality: 'major' },
{ rootOffset: 5, quality: 'major' },
{ rootOffset: 9, quality: 'minor' },
{ rootOffset: 7, quality: 'major' },
],
},
},
];
const audioVibes = Object.fromEntries(
vibePresets.map((vibe) => [vibe.id, vibe.audio])
) as Record<string, GardenAudioVibeProfile>;
export const appConfig: GardenAppConfig = {
audio: {
masterVolume: 0.32,
fadeInSeconds: 0.45,
updateRampSeconds: 0.08,
highPassFrequencyHz: 45,
fallbackVibeId: defaultVibeId,
compressor: {
thresholdDb: -18,
kneeDb: 18,
ratio: 2.4,
attackSeconds: 0.006,
releaseSeconds: 0.18,
},
delay: {
timeSeconds: 0.42,
feedback: 0.12,
wetGain: 0.048,
},
piano: {
maxVoices: 32,
gain: 0.42,
sustainSeconds: 0.52,
sustainLevel: 0.34,
releaseSeconds: 0.16,
lowpassHz: 9000,
},
input: {
pressureFallback: 0.48,
},
rhythm: {
bpm: 82,
stepsPerBeat: 4,
stepsPerBar: 16,
lookaheadSeconds: 0.18,
speedForFullEnergyPixelsPerSecond: 1800,
sparseActivity: 0.055,
},
eraser: {
minIntervalSeconds: 0.12,
noiseGain: 0.028,
filterMinHz: 650,
filterMaxHz: 3600,
},
colorVoices: [
{
scaleDegreeOffset: 0,
velocityMultiplier: 0.92,
panOffset: -0.14,
},
{
scaleDegreeOffset: 1,
velocityMultiplier: 1,
panOffset: 0,
},
{
scaleDegreeOffset: 2,
velocityMultiplier: 0.86,
panOffset: 0.14,
},
],
vibes: audioVibes,
},
audioEngine: {
energy: {
attackSeconds: 0.08,
decaySeconds: 0.9,
releaseSeconds: 1.15,
strokeDecaySeconds: 0.32,
},
eraser: {
canvasWidthRatioForFullSize: 0.18,
defaultSizePixels: 96,
durationSeconds: 0.08,
filterPressureWeight: 0.26,
filterSizeWeight: 0.16,
filterSpeedWeight: 0.58,
gainBase: 0.45,
gainPressureWeight: 0.24,
gainSizeWeight: 0.18,
gainSpeedWeight: 0.38,
},
delay: {
erasingActivity: 0.12,
},
gestureFadeSeconds: 1.35,
graph: {
closeGain: 0.0001,
closeRampSeconds: 0.015,
delayActivityFeedbackWeight: 0.08,
delayFeedbackMax: 0.32,
delayFeedbackMin: 0.04,
delayOutputActivityWeight: 0.5,
delayOutputBase: 0.65,
delayTimeRampSeconds: 0.12,
eventBusGain: 1,
noiseMax: 1,
noiseMin: -1,
unlockBufferLength: 1,
unlockSampleRate: 22050,
},
input: {
distanceEnergyBase: 0.34,
distanceEnergyScale: 0.66,
distanceForFullEnergyPixels: 140,
fallbackFrameSeconds: 1 / 60,
penMinPressure: 0.56,
strokeEnergyBase: 0.18,
strokeEnergyPressureWeight: 0.22,
strokeEnergySpeedWeight: 0.62,
},
muteGain: 0.0001,
muteRampSeconds: 0.02,
noiseBurst: {
attackSeconds: 0.004,
filterQ: 1.4,
offsetRandomSeconds: 0.4,
scheduleAheadSeconds: 0.002,
silentGain: 0.0001,
},
piano: {
fadeStopExtraSeconds: 0.05,
defaultFadeSeconds: 0.9,
fadeTimeConstantRatio: 0.3,
filterQ: 0.7,
gainAttackSeconds: 0.006,
lowpassMaxHz: 12000,
lowpassMinHz: 1400,
minDurationSeconds: 0.08,
minFadeSeconds: 0.08,
minGain: 0.0001,
pitchSemitonesPerOctave: 12,
scheduleAheadSeconds: 0.002,
sustainBase: 0.45,
sustainVelocityRange: 0.55,
tailStopExtraSeconds: 0.05,
voiceStealFadeSeconds: 0.025,
voiceStealStopSeconds: 0.05,
},
startDelaySeconds: 0.02,
vibeChangeStingerMinIntervalSeconds: 0.45,
},
deltaTime: {
fpsExponentialDecayStrength: 0.01,
maxDeltaTimeSeconds: 1 / 30,
minDeltaTimeSeconds: 1 / 240,
},
export4k: {
bytesPerPixel: 4,
height: 2160,
jsHeapSafetyMultiplier: 1.5,
lowMemoryDeviceGiB: 2,
lowMemoryExportFraction: 0.08,
rowAlignmentBytes: 256,
width: 3840,
},
menuHider: {
bottomRevealDistancePx: 96,
intervalMs: 50,
timeToLiveMs: 3500,
},
pipelines: {
brush: {
maxLineCount: 240,
},
diffusion: {
minDiffusionRate: 0.000001,
},
eraser: {
maxSegmentCount: 384,
maxTextureLineCount: 384,
segmentFloatCount: 4,
workgroupSize: 64,
},
},
runtimeSettings: {
defaults: {
agentBudgetMax: 1_000_000,
agentCount: 0,
selectedColorIndex: 0,
spawnPerPixel: 0.22,
moveSpeed: 82,
turnSpeed: 58,
sensorOffsetAngle: 34,
sensorOffsetDistance: 38,
turnWhenLost: 0.8,
individualTrailWeight: 0.07,
diffusionRateTrails: 0.22,
decayRateTrails: 965,
diffusionRateBrush: 0.35,
decayRateBrush: 18,
brushEffectDuration: 8,
clarity: 0.62,
brushSize: 14,
eraserSize: 96,
mirrorSegmentCount: 1,
brushSizeVariation: 0.5,
startColorHue: 200,
renderSpeed: 1,
simulatedDelayMs: 0,
},
controls: {
agentBudgetMax: {
folder: 'Runtime',
integer: true,
min: 1_000,
max: 1_000_000,
step: 1_000,
},
agentCount: {
folder: 'Runtime',
integer: true,
min: 0,
max: 1_000_000,
step: 1_000,
},
brushEffectDuration: {
folder: 'Diffusion',
min: 0.5,
max: 20,
step: 0.05,
},
brushSize: {
folder: 'Brush',
min: 1,
max: 60,
step: 0.25,
},
brushSizeVariation: {
folder: 'Brush',
min: 0,
max: 1,
step: 0.01,
},
clarity: {
folder: 'Render',
min: 0.00001,
max: 1,
step: 0.001,
},
decayRateBrush: {
folder: 'Diffusion',
min: 0.1,
max: 100,
step: 0.1,
},
decayRateTrails: {
folder: 'Diffusion',
min: 0.1,
max: 5000,
step: 1,
},
diffusionRateBrush: {
folder: 'Diffusion',
min: 0.001,
max: 1,
step: 0.001,
},
diffusionRateTrails: {
folder: 'Diffusion',
min: 0,
max: 2,
step: 0.001,
},
eraserSize: {
folder: 'Brush',
integer: true,
min: 24,
max: 240,
step: 1,
},
individualTrailWeight: {
folder: 'Agent',
min: 0,
max: 1,
step: 0.001,
},
mirrorSegmentCount: {
folder: 'Brush',
integer: true,
min: 1,
max: 12,
step: 1,
},
moveSpeed: {
folder: 'Agent',
min: 10,
max: 500,
step: 1,
},
renderSpeed: {
folder: 'Runtime',
integer: true,
min: 1,
max: 10,
step: 1,
},
selectedColorIndex: {
folder: 'Brush',
integer: true,
min: 0,
max: 2,
step: 1,
},
sensorOffsetAngle: {
folder: 'Agent',
min: 0,
max: 90,
step: 1,
},
sensorOffsetDistance: {
folder: 'Agent',
min: 0,
max: 200,
step: 1,
},
simulatedDelayMs: {
folder: 'Runtime',
integer: true,
min: 0,
max: 2000,
step: 1,
},
spawnPerPixel: {
folder: 'Agent',
min: 0.01,
max: 1,
step: 0.001,
},
startColorHue: {
folder: 'Render',
min: 0,
max: 360,
step: 1,
},
turnSpeed: {
folder: 'Agent',
min: 1,
max: 200,
step: 1,
},
turnWhenLost: {
folder: 'Agent',
min: 0,
max: 1,
step: 0.001,
},
},
},
simulation: {
budget: {
fpsHeadroom: 0.82,
fpsSmoothingNew: 0.06,
fpsSmoothingRetain: 0.94,
initialTargetAgentBudget: 20_000,
rampAgentsPerSecond: 20_000,
refreshTargetDecay: 0.995,
},
brushEffectFramesPerSecond: 60,
globalAgentCap: 1_000_000,
initialAgentCount: 180_000,
intro: {
angleJitterRadians: Math.PI * 0.08,
circleMaxSideRatio: 0.46,
circleMinSideRatio: 0.32,
drawHintClass: 'draw-hint',
drawHintDelayMs: 3000,
durationSeconds: 4,
entryJitterSideRatio: 0.035,
fontScaleDown: 0.94,
initialFontHeightRatio: 0.28,
initialFontWidthRatio: 0.19,
letterSpacingEm: 0.07,
maskAlphaThreshold: 32,
maskGradientThreshold: 8,
maskSampleDensity: 540,
maxHeightRatio: 0.25,
maxWidthRatio: 0.76,
minEntryJitterPx: 6,
minFontSizePx: 18,
minTargetJitterPx: 1,
radialJitterRatio: 0.35,
targetDelayDistanceMultiplier: 0.12,
targetDelayMax: 0.22,
targetDelayRandomMultiplier: 0.06,
targetJitterSideRatio: 0.0035,
title: 'Fleeting',
titleColorCutLetters: [2, 5],
titleRadiusMultiplier: 1.55,
titleStrokeWidthMinPx: 6,
titleStrokeWidthRatio: 0.11,
verticalAnchor: 0.47,
},
introCameraZoom: 0.12,
introMoveSpeedBaseMultiplier: 1.8,
introMoveSpeedProgressMultiplier: 0.35,
maxMirrorSegmentCount: 12,
stroke: {
angleJitterRadians: Math.PI * 0.7,
densityMultiplier: 110,
maxAgentCount: 2_400,
minAgentCount: 140,
},
},
storage: {
audioMutedKey: 'fleeting-garden:audio-muted',
vibeKey: 'fleeting-garden:vibe',
},
telemetry: {
enabled: false,
intervalMs: 1000,
},
toolbar: {
eraser: {
controlScaleMax: 1.34,
controlScaleMin: 0.74,
default: 96,
max: 240,
min: 24,
step: 1,
},
mirror: {
default: 1,
max: 12,
min: 1,
names: {
2: 'halves',
3: 'thirds',
4: 'quarters',
5: 'fifths',
6: 'sixths',
7: 'sevenths',
8: 'eighths',
9: 'ninths',
10: 'tenths',
11: 'elevenths',
12: 'twelfths',
},
step: 1,
},
},
tuningPane: {
expandedDepth: 1,
startHidden: true,
title: 'Garden Config',
},
vibes: {
defaultVibeId,
presets: vibePresets,
},
};