more clean up
This commit is contained in:
parent
15e99380b5
commit
ea0304356f
11 changed files with 710 additions and 0 deletions
30
eslint.config.js
Normal file
30
eslint.config.js
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import js from '@eslint/js';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
import prettierConfig from 'eslint-config-prettier';
|
||||||
|
import globals from 'globals';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{
|
||||||
|
ignores: ['node_modules/**', 'dist/**', 'public/**'],
|
||||||
|
},
|
||||||
|
js.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
prettierConfig,
|
||||||
|
{
|
||||||
|
files: ['**/*.ts'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2022,
|
||||||
|
sourceType: 'module',
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-explicit-any': 'error',
|
||||||
|
'@typescript-eslint/no-empty-function': 'off',
|
||||||
|
'@typescript-eslint/no-non-null-assertion': 'error',
|
||||||
|
'@typescript-eslint/ban-ts-comment': 'error',
|
||||||
|
'prefer-const': 'error',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
13
src/app-constants.ts
Normal file
13
src/app-constants.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
export const ENABLED_FLAG_VALUE = '1';
|
||||||
|
export const DISABLED_FLAG_VALUE = '0';
|
||||||
|
|
||||||
|
export const UNIT_INTERVAL_INPUT_MIN = '0';
|
||||||
|
export const UNIT_INTERVAL_INPUT_MAX = '1';
|
||||||
|
|
||||||
|
export const DEFAULT_AUDIO_VOLUME = 0.42;
|
||||||
|
|
||||||
|
export const APP_STORAGE_KEYS = {
|
||||||
|
audioMuted: 'fleeting-garden:audio-muted',
|
||||||
|
audioVolume: 'fleeting-garden:audio-volume',
|
||||||
|
vibe: 'fleeting-garden:vibe',
|
||||||
|
} as const;
|
||||||
38
src/audio/audio-pan-node.ts
Normal file
38
src/audio/audio-pan-node.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { clamp } from '../utils/math';
|
||||||
|
import { isIosLike } from './audio-platform';
|
||||||
|
|
||||||
|
interface AudioPanNode {
|
||||||
|
input: AudioNode;
|
||||||
|
output: AudioNode;
|
||||||
|
disconnect: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type AudioContextWithOptionalPanner = AudioContext & {
|
||||||
|
createStereoPanner?: () => StereoPannerNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createAudioPanNode = (
|
||||||
|
context: AudioContext,
|
||||||
|
pan: number,
|
||||||
|
startTime: number
|
||||||
|
): AudioPanNode => {
|
||||||
|
const createStereoPanner = (context as AudioContextWithOptionalPanner)
|
||||||
|
.createStereoPanner;
|
||||||
|
|
||||||
|
if (!isIosLike() && typeof createStereoPanner === 'function') {
|
||||||
|
const panner = createStereoPanner.call(context);
|
||||||
|
panner.pan.setValueAtTime(clamp(pan, -1, 1), startTime);
|
||||||
|
return {
|
||||||
|
input: panner,
|
||||||
|
output: panner,
|
||||||
|
disconnect: () => panner.disconnect(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const passthrough = context.createGain();
|
||||||
|
return {
|
||||||
|
input: passthrough,
|
||||||
|
output: passthrough,
|
||||||
|
disconnect: () => passthrough.disconnect(),
|
||||||
|
};
|
||||||
|
};
|
||||||
3
src/audio/audio-platform.ts
Normal file
3
src/audio/audio-platform.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export const isIosLike = (): boolean =>
|
||||||
|
/iPad|iPhone|iPod/.test(navigator.userAgent) ||
|
||||||
|
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
||||||
3
src/audio/garden-audio-scheduling.ts
Normal file
3
src/audio/garden-audio-scheduling.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export const GENERATIVE_LOOKAHEAD_SECONDS = 0.3;
|
||||||
|
export const GENERATIVE_START_DELAY_SECONDS = 0.02;
|
||||||
|
export const PIANO_SCHEDULE_AHEAD_SECONDS = 0.002;
|
||||||
427
src/audio/generative-piano-tuning.ts
Normal file
427
src/audio/generative-piano-tuning.ts
Normal file
|
|
@ -0,0 +1,427 @@
|
||||||
|
export interface GardenAudioRegister {
|
||||||
|
midiMin: number;
|
||||||
|
midiMax: number;
|
||||||
|
preferredMidi: number;
|
||||||
|
pan: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GardenAudioStylePool extends GardenAudioRegister {
|
||||||
|
scaleDegrees: Array<number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GardenAudioStyleVoice {
|
||||||
|
scaleDegreeOffset: number;
|
||||||
|
velocityMultiplier: number;
|
||||||
|
panOffset: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GenerativePianoTuning {
|
||||||
|
stylePools: [GardenAudioStylePool, GardenAudioStylePool, GardenAudioStylePool];
|
||||||
|
padRegisters: [GardenAudioRegister, GardenAudioRegister, GardenAudioRegister];
|
||||||
|
vibeChangeStinger: {
|
||||||
|
velocities: [number, number, number];
|
||||||
|
pans: [number, number, number];
|
||||||
|
delaySends: [number, number, number];
|
||||||
|
lowpassExpression: number;
|
||||||
|
};
|
||||||
|
highActivityExtra: {
|
||||||
|
barOffset: number;
|
||||||
|
expressionMultiplier: number;
|
||||||
|
};
|
||||||
|
padChord: {
|
||||||
|
velocities: [number, number, number];
|
||||||
|
expressionVelocityWeight: number;
|
||||||
|
delaySend: number;
|
||||||
|
lowpassExpressionWeight: number;
|
||||||
|
};
|
||||||
|
supportNote: {
|
||||||
|
velocityBase: number;
|
||||||
|
velocityExpressionWeight: number;
|
||||||
|
durationBaseSeconds: number;
|
||||||
|
durationExpressionSeconds: number;
|
||||||
|
delaySendBase: number;
|
||||||
|
delaySendExpressionWeight: number;
|
||||||
|
lowpassExpressionWeight: number;
|
||||||
|
expressionThreshold: number;
|
||||||
|
offsetsByStyle: [Array<number>, Array<number>, Array<number>];
|
||||||
|
};
|
||||||
|
textureNote: {
|
||||||
|
velocityBase: number;
|
||||||
|
velocityExpressionWeight: number;
|
||||||
|
durationBaseSeconds: number;
|
||||||
|
durationExpressionSeconds: number;
|
||||||
|
delaySendBase: number;
|
||||||
|
delaySendExpressionWeight: number;
|
||||||
|
idleExpressionThreshold: number;
|
||||||
|
mediumExpressionThreshold: number;
|
||||||
|
intenseSpacing: number;
|
||||||
|
idlePhase: number;
|
||||||
|
};
|
||||||
|
gestureAccent: {
|
||||||
|
rotationStrengthMultiplier: number;
|
||||||
|
quantizeStepLookahead: number;
|
||||||
|
velocityBase: number;
|
||||||
|
velocityStrengthWeight: number;
|
||||||
|
durationBaseSeconds: number;
|
||||||
|
durationStrengthSeconds: number;
|
||||||
|
delaySend: number;
|
||||||
|
};
|
||||||
|
touchNote: {
|
||||||
|
registerBiasManiaAmount: number;
|
||||||
|
velocityBase: number;
|
||||||
|
velocityStrengthWeight: number;
|
||||||
|
durationBaseSeconds: number;
|
||||||
|
durationStrengthSeconds: number;
|
||||||
|
delaySend: number;
|
||||||
|
lowpassBaseExpression: number;
|
||||||
|
lowpassStrengthWeight: number;
|
||||||
|
};
|
||||||
|
brushPhrase: {
|
||||||
|
initialMotifOffset: number;
|
||||||
|
energyDecaySeconds: number;
|
||||||
|
maniaDecaySeconds: number;
|
||||||
|
fadeMinimumLifetimeSeconds: number;
|
||||||
|
layerIntensityBase: number;
|
||||||
|
layerIntensityManiaWeight: number;
|
||||||
|
frameActivityWeight: number;
|
||||||
|
frameManiaWeight: number;
|
||||||
|
};
|
||||||
|
brushStream: {
|
||||||
|
inferredManiaThreshold: number;
|
||||||
|
inferredManiaRange: number;
|
||||||
|
registerManiaShift: number;
|
||||||
|
chordToneEverySteps: number;
|
||||||
|
durationBaseSeconds: number;
|
||||||
|
durationIntensitySeconds: number;
|
||||||
|
durationManiaSeconds: number;
|
||||||
|
durationMinSeconds: number;
|
||||||
|
durationMaxSeconds: number;
|
||||||
|
delaySendBase: number;
|
||||||
|
delaySendIntensityWeight: number;
|
||||||
|
delaySendManiaWeight: number;
|
||||||
|
delaySendMin: number;
|
||||||
|
delaySendMax: number;
|
||||||
|
velocityBase: number;
|
||||||
|
velocityIntensityWeight: number;
|
||||||
|
lowpassBaseExpression: number;
|
||||||
|
lowpassIntensityWeight: number;
|
||||||
|
lowpassManiaWeight: number;
|
||||||
|
manicThreshold: number;
|
||||||
|
intenseThreshold: number;
|
||||||
|
activeThreshold: number;
|
||||||
|
};
|
||||||
|
brushStreamEcho: {
|
||||||
|
maniaThreshold: number;
|
||||||
|
stepModulo: number;
|
||||||
|
stepRemainder: number;
|
||||||
|
intensityThreshold: number;
|
||||||
|
octaveSemitones: number;
|
||||||
|
maxMidi: number;
|
||||||
|
velocityBase: number;
|
||||||
|
velocityIntensityWeight: number;
|
||||||
|
durationMinSeconds: number;
|
||||||
|
durationScale: number;
|
||||||
|
panScale: number;
|
||||||
|
delaySendMin: number;
|
||||||
|
delaySendScale: number;
|
||||||
|
lowpassBaseExpression: number;
|
||||||
|
lowpassManiaWeight: number;
|
||||||
|
};
|
||||||
|
brushMotif: {
|
||||||
|
highThreshold: number;
|
||||||
|
mediumThreshold: number;
|
||||||
|
highOffset: number;
|
||||||
|
mediumOffset: number;
|
||||||
|
lowOffset: number;
|
||||||
|
minOffset: number;
|
||||||
|
maxOffset: number;
|
||||||
|
};
|
||||||
|
registerBias: {
|
||||||
|
maniaShiftSemitones: number;
|
||||||
|
midiMin: number;
|
||||||
|
midiMaxForMin: number;
|
||||||
|
minimumSpan: number;
|
||||||
|
midiMax: number;
|
||||||
|
};
|
||||||
|
candidateOctaveSearch: {
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
};
|
||||||
|
stylePanOffsetScale: number;
|
||||||
|
lowpass: {
|
||||||
|
midiBase: number;
|
||||||
|
midiRange: number;
|
||||||
|
midiLiftHz: number;
|
||||||
|
expressionBase: number;
|
||||||
|
expressionWeight: number;
|
||||||
|
};
|
||||||
|
styleRotationBars: number;
|
||||||
|
chordBars: number;
|
||||||
|
supportBarSpacing: number;
|
||||||
|
supportBarOffset: number;
|
||||||
|
idleTextureBarSpacing: number;
|
||||||
|
mediumTextureBarSpacing: number;
|
||||||
|
textureBeat: number;
|
||||||
|
highActivityExtraBeat: number;
|
||||||
|
highActivityExtraThreshold: number;
|
||||||
|
noteScorePreferenceWeight: number;
|
||||||
|
noteScoreRegisterWeight: number;
|
||||||
|
noteScoreChordToneWeight: number;
|
||||||
|
noteScoreRepeatPenalty: number;
|
||||||
|
gestureAccentMinIntervalSeconds: number;
|
||||||
|
strokeAccentMinSteps: number;
|
||||||
|
strokeAccentThreshold: number;
|
||||||
|
stingerDurationSeconds: number;
|
||||||
|
stingerSpacingSeconds: number;
|
||||||
|
maxBrushPhraseLayers: number;
|
||||||
|
maxBrushStreamNotesPerBar: number;
|
||||||
|
brushLayerBaseSeconds: number;
|
||||||
|
brushLayerEnergySeconds: number;
|
||||||
|
brushLayerMinIntensity: number;
|
||||||
|
brushStreamIdleIntervalBeats: number;
|
||||||
|
brushStreamActiveIntervalBeats: number;
|
||||||
|
brushStreamIntenseIntervalBeats: number;
|
||||||
|
brushStreamManicIntervalBeats: number;
|
||||||
|
brushMotifMaxSteps: number;
|
||||||
|
brushMotifCanonDelaySeconds: number;
|
||||||
|
padDurationBarScale: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generativePianoTuning: GenerativePianoTuning = {
|
||||||
|
stylePools: [
|
||||||
|
{
|
||||||
|
midiMin: 48,
|
||||||
|
midiMax: 67,
|
||||||
|
preferredMidi: 55,
|
||||||
|
pan: -0.18,
|
||||||
|
scaleDegrees: [0, 1, 2, 4],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
midiMin: 55,
|
||||||
|
midiMax: 74,
|
||||||
|
preferredMidi: 63,
|
||||||
|
pan: 0,
|
||||||
|
scaleDegrees: [1, 2, 3, 5],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
midiMin: 62,
|
||||||
|
midiMax: 81,
|
||||||
|
preferredMidi: 72,
|
||||||
|
pan: 0.18,
|
||||||
|
scaleDegrees: [2, 3, 4, 6],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
padRegisters: [
|
||||||
|
{
|
||||||
|
midiMin: 40,
|
||||||
|
midiMax: 55,
|
||||||
|
preferredMidi: 48,
|
||||||
|
pan: -0.12,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
midiMin: 48,
|
||||||
|
midiMax: 64,
|
||||||
|
preferredMidi: 55,
|
||||||
|
pan: 0.08,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
midiMin: 58,
|
||||||
|
midiMax: 76,
|
||||||
|
preferredMidi: 67,
|
||||||
|
pan: 0.2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vibeChangeStinger: {
|
||||||
|
velocities: [0.1, 0.085, 0.07],
|
||||||
|
pans: [-0.16, 0, 0.16],
|
||||||
|
delaySends: [0.012, 0.014, 0.016],
|
||||||
|
lowpassExpression: 0.35,
|
||||||
|
},
|
||||||
|
highActivityExtra: {
|
||||||
|
barOffset: 1,
|
||||||
|
expressionMultiplier: 0.9,
|
||||||
|
},
|
||||||
|
padChord: {
|
||||||
|
velocities: [0.052, 0.041, 0.033],
|
||||||
|
expressionVelocityWeight: 0.02,
|
||||||
|
delaySend: 0.008,
|
||||||
|
lowpassExpressionWeight: 0.28,
|
||||||
|
},
|
||||||
|
supportNote: {
|
||||||
|
velocityBase: 0.105,
|
||||||
|
velocityExpressionWeight: 0.07,
|
||||||
|
durationBaseSeconds: 1.35,
|
||||||
|
durationExpressionSeconds: 0.4,
|
||||||
|
delaySendBase: 0.016,
|
||||||
|
delaySendExpressionWeight: 0.006,
|
||||||
|
lowpassExpressionWeight: 0.7,
|
||||||
|
expressionThreshold: 0.55,
|
||||||
|
offsetsByStyle: [
|
||||||
|
[0, 2, 12],
|
||||||
|
[1, 2, 0, 12],
|
||||||
|
[2, 12, 3, 13],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
textureNote: {
|
||||||
|
velocityBase: 0.09,
|
||||||
|
velocityExpressionWeight: 0.08,
|
||||||
|
durationBaseSeconds: 0.62,
|
||||||
|
durationExpressionSeconds: 0.24,
|
||||||
|
delaySendBase: 0.016,
|
||||||
|
delaySendExpressionWeight: 0.006,
|
||||||
|
idleExpressionThreshold: 0.35,
|
||||||
|
mediumExpressionThreshold: 0.7,
|
||||||
|
intenseSpacing: 1,
|
||||||
|
idlePhase: 1,
|
||||||
|
},
|
||||||
|
gestureAccent: {
|
||||||
|
rotationStrengthMultiplier: 3,
|
||||||
|
quantizeStepLookahead: 1,
|
||||||
|
velocityBase: 0.12,
|
||||||
|
velocityStrengthWeight: 0.09,
|
||||||
|
durationBaseSeconds: 0.48,
|
||||||
|
durationStrengthSeconds: 0.22,
|
||||||
|
delaySend: 0.012,
|
||||||
|
},
|
||||||
|
touchNote: {
|
||||||
|
registerBiasManiaAmount: 0,
|
||||||
|
velocityBase: 0.14,
|
||||||
|
velocityStrengthWeight: 0.11,
|
||||||
|
durationBaseSeconds: 0.55,
|
||||||
|
durationStrengthSeconds: 0.18,
|
||||||
|
delaySend: 0.006,
|
||||||
|
lowpassBaseExpression: 0.55,
|
||||||
|
lowpassStrengthWeight: 0.35,
|
||||||
|
},
|
||||||
|
brushPhrase: {
|
||||||
|
initialMotifOffset: -1,
|
||||||
|
energyDecaySeconds: 0.72,
|
||||||
|
maniaDecaySeconds: 0.54,
|
||||||
|
fadeMinimumLifetimeSeconds: 0.001,
|
||||||
|
layerIntensityBase: 0.8,
|
||||||
|
layerIntensityManiaWeight: 0.42,
|
||||||
|
frameActivityWeight: 0.42,
|
||||||
|
frameManiaWeight: 0.18,
|
||||||
|
},
|
||||||
|
brushStream: {
|
||||||
|
inferredManiaThreshold: 0.82,
|
||||||
|
inferredManiaRange: 0.18,
|
||||||
|
registerManiaShift: 0.45,
|
||||||
|
chordToneEverySteps: 4,
|
||||||
|
durationBaseSeconds: 0.48,
|
||||||
|
durationIntensitySeconds: 0.08,
|
||||||
|
durationManiaSeconds: 0.34,
|
||||||
|
durationMinSeconds: 0.14,
|
||||||
|
durationMaxSeconds: 0.62,
|
||||||
|
delaySendBase: 0.012,
|
||||||
|
delaySendIntensityWeight: 0.011,
|
||||||
|
delaySendManiaWeight: 0.006,
|
||||||
|
delaySendMin: 0.006,
|
||||||
|
delaySendMax: 0.032,
|
||||||
|
velocityBase: 0.1,
|
||||||
|
velocityIntensityWeight: 0.13,
|
||||||
|
lowpassBaseExpression: 0.39,
|
||||||
|
lowpassIntensityWeight: 0.48,
|
||||||
|
lowpassManiaWeight: 0.18,
|
||||||
|
manicThreshold: 0.85,
|
||||||
|
intenseThreshold: 0.62,
|
||||||
|
activeThreshold: 0.34,
|
||||||
|
},
|
||||||
|
brushStreamEcho: {
|
||||||
|
maniaThreshold: 0.86,
|
||||||
|
stepModulo: 2,
|
||||||
|
stepRemainder: 1,
|
||||||
|
intensityThreshold: 0.95,
|
||||||
|
octaveSemitones: 12,
|
||||||
|
maxMidi: 88,
|
||||||
|
velocityBase: 0.045,
|
||||||
|
velocityIntensityWeight: 0.05,
|
||||||
|
durationMinSeconds: 0.11,
|
||||||
|
durationScale: 0.68,
|
||||||
|
panScale: -0.75,
|
||||||
|
delaySendMin: 0.006,
|
||||||
|
delaySendScale: 0.72,
|
||||||
|
lowpassBaseExpression: 0.62,
|
||||||
|
lowpassManiaWeight: 0.24,
|
||||||
|
},
|
||||||
|
brushMotif: {
|
||||||
|
highThreshold: 0.82,
|
||||||
|
mediumThreshold: 0.55,
|
||||||
|
highOffset: 1,
|
||||||
|
mediumOffset: 0,
|
||||||
|
lowOffset: -1,
|
||||||
|
minOffset: -3,
|
||||||
|
maxOffset: 4,
|
||||||
|
},
|
||||||
|
registerBias: {
|
||||||
|
maniaShiftSemitones: 4,
|
||||||
|
midiMin: 36,
|
||||||
|
midiMaxForMin: 86,
|
||||||
|
minimumSpan: 4,
|
||||||
|
midiMax: 91,
|
||||||
|
},
|
||||||
|
candidateOctaveSearch: {
|
||||||
|
min: -3,
|
||||||
|
max: 3,
|
||||||
|
},
|
||||||
|
stylePanOffsetScale: 0.35,
|
||||||
|
lowpass: {
|
||||||
|
midiBase: 48,
|
||||||
|
midiRange: 33,
|
||||||
|
midiLiftHz: 720,
|
||||||
|
expressionBase: 0.58,
|
||||||
|
expressionWeight: 0.32,
|
||||||
|
},
|
||||||
|
styleRotationBars: 2,
|
||||||
|
chordBars: 4,
|
||||||
|
supportBarSpacing: 2,
|
||||||
|
supportBarOffset: 1,
|
||||||
|
idleTextureBarSpacing: 2,
|
||||||
|
mediumTextureBarSpacing: 1,
|
||||||
|
textureBeat: 2,
|
||||||
|
highActivityExtraBeat: 3,
|
||||||
|
highActivityExtraThreshold: 0.45,
|
||||||
|
noteScorePreferenceWeight: 1.8,
|
||||||
|
noteScoreRegisterWeight: 0.28,
|
||||||
|
noteScoreChordToneWeight: 0.75,
|
||||||
|
noteScoreRepeatPenalty: 3.2,
|
||||||
|
gestureAccentMinIntervalSeconds: 2.5,
|
||||||
|
strokeAccentMinSteps: 12,
|
||||||
|
strokeAccentThreshold: 0.58,
|
||||||
|
stingerSpacingSeconds: 0.08,
|
||||||
|
stingerDurationSeconds: 1.1,
|
||||||
|
maxBrushPhraseLayers: 3,
|
||||||
|
maxBrushStreamNotesPerBar: 9,
|
||||||
|
brushLayerBaseSeconds: 5.5,
|
||||||
|
brushLayerEnergySeconds: 2.5,
|
||||||
|
brushLayerMinIntensity: 0.12,
|
||||||
|
brushStreamIdleIntervalBeats: 2,
|
||||||
|
brushStreamActiveIntervalBeats: 1,
|
||||||
|
brushStreamIntenseIntervalBeats: 0.5,
|
||||||
|
brushStreamManicIntervalBeats: 0.5,
|
||||||
|
brushMotifMaxSteps: 8,
|
||||||
|
brushMotifCanonDelaySeconds: 0.055,
|
||||||
|
padDurationBarScale: 0.46,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const styleVoices: [
|
||||||
|
GardenAudioStyleVoice,
|
||||||
|
GardenAudioStyleVoice,
|
||||||
|
GardenAudioStyleVoice,
|
||||||
|
] = [
|
||||||
|
{
|
||||||
|
scaleDegreeOffset: 0,
|
||||||
|
velocityMultiplier: 0.92,
|
||||||
|
panOffset: -0.14,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scaleDegreeOffset: 1,
|
||||||
|
velocityMultiplier: 1,
|
||||||
|
panOffset: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scaleDegreeOffset: 2,
|
||||||
|
velocityMultiplier: 0.86,
|
||||||
|
panOffset: 0.14,
|
||||||
|
},
|
||||||
|
];
|
||||||
68
src/game-loop/dev-stats-overlay.ts
Normal file
68
src/game-loop/dev-stats-overlay.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
const DEV_STATS_REFRESH_MS = 200;
|
||||||
|
const ZERO_STAT_TEXT = '0';
|
||||||
|
const ZERO_FRAME_TIME_TEXT = '0ms';
|
||||||
|
const ZERO_RESOLUTION_TEXT = '0x0';
|
||||||
|
|
||||||
|
interface DevStatsSnapshot {
|
||||||
|
time: DOMHighResTimeStamp;
|
||||||
|
fps: number;
|
||||||
|
agentCount: number;
|
||||||
|
frameTimeMs: number;
|
||||||
|
renderWidth: number;
|
||||||
|
renderHeight: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DevStatsOverlay {
|
||||||
|
private readonly element: HTMLDivElement;
|
||||||
|
private previousUpdateTime = Number.NEGATIVE_INFINITY;
|
||||||
|
private previousText = '';
|
||||||
|
|
||||||
|
public constructor(parent: HTMLElement) {
|
||||||
|
this.element = document.createElement('div');
|
||||||
|
this.element.className = 'dev-stats-overlay';
|
||||||
|
this.element.setAttribute('aria-hidden', 'true');
|
||||||
|
parent.append(this.element);
|
||||||
|
}
|
||||||
|
|
||||||
|
public update({
|
||||||
|
time,
|
||||||
|
fps,
|
||||||
|
agentCount,
|
||||||
|
frameTimeMs,
|
||||||
|
renderWidth,
|
||||||
|
renderHeight,
|
||||||
|
}: DevStatsSnapshot): void {
|
||||||
|
if (time - this.previousUpdateTime < DEV_STATS_REFRESH_MS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.previousUpdateTime = time;
|
||||||
|
const text = `FPS ${formatFps(fps)}\nAgents ${formatAgentCount(agentCount)}\nFrame ${formatFrameTime(frameTimeMs)}\nResolution ${formatResolution(renderWidth, renderHeight)}`;
|
||||||
|
if (text !== this.previousText) {
|
||||||
|
this.element.textContent = text;
|
||||||
|
this.previousText = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroy(): void {
|
||||||
|
this.element.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatFps = (fps: number): string =>
|
||||||
|
Number.isFinite(fps) ? Math.max(0, Math.round(fps)).toString() : ZERO_STAT_TEXT;
|
||||||
|
|
||||||
|
const formatAgentCount = (agentCount: number): string =>
|
||||||
|
Number.isFinite(agentCount)
|
||||||
|
? Math.max(0, Math.round(agentCount)).toLocaleString('en-US')
|
||||||
|
: ZERO_STAT_TEXT;
|
||||||
|
|
||||||
|
const formatFrameTime = (frameTimeMs: number): string =>
|
||||||
|
Number.isFinite(frameTimeMs)
|
||||||
|
? `${Math.max(0, frameTimeMs).toFixed(frameTimeMs < 10 ? 1 : 0)}ms`
|
||||||
|
: ZERO_FRAME_TIME_TEXT;
|
||||||
|
|
||||||
|
const formatResolution = (width: number, height: number): string =>
|
||||||
|
Number.isFinite(width) && Number.isFinite(height)
|
||||||
|
? `${Math.max(0, Math.round(width))}x${Math.max(0, Math.round(height))}`
|
||||||
|
: ZERO_RESOLUTION_TEXT;
|
||||||
53
src/game-loop/internal-render-size.ts
Normal file
53
src/game-loop/internal-render-size.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
const MEGAPIXEL = 1_000_000;
|
||||||
|
|
||||||
|
export interface InternalRenderSizeOptions {
|
||||||
|
clientHeight: number;
|
||||||
|
clientWidth: number;
|
||||||
|
maxPixelScale: number;
|
||||||
|
maxTextureDimension: number;
|
||||||
|
targetAreaMegapixels: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InternalRenderSize {
|
||||||
|
height: number;
|
||||||
|
width: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSafeInternalRenderAreaMegapixels = (targetAreaMegapixels: number): number =>
|
||||||
|
Number.isFinite(targetAreaMegapixels) && targetAreaMegapixels > 0
|
||||||
|
? targetAreaMegapixels
|
||||||
|
: 1;
|
||||||
|
|
||||||
|
const getSafeMaxPixelScale = (maxPixelScale: number): number =>
|
||||||
|
Number.isFinite(maxPixelScale) && maxPixelScale > 0
|
||||||
|
? maxPixelScale
|
||||||
|
: Number.POSITIVE_INFINITY;
|
||||||
|
|
||||||
|
export const getInternalRenderSize = ({
|
||||||
|
clientHeight,
|
||||||
|
clientWidth,
|
||||||
|
maxPixelScale,
|
||||||
|
maxTextureDimension,
|
||||||
|
targetAreaMegapixels,
|
||||||
|
}: InternalRenderSizeOptions): InternalRenderSize => {
|
||||||
|
const safeClientWidth = Math.max(1, clientWidth);
|
||||||
|
const safeClientHeight = Math.max(1, clientHeight);
|
||||||
|
const safeMaxTextureDimension =
|
||||||
|
Number.isFinite(maxTextureDimension) && maxTextureDimension > 0
|
||||||
|
? Math.floor(maxTextureDimension)
|
||||||
|
: Number.POSITIVE_INFINITY;
|
||||||
|
const targetArea =
|
||||||
|
getSafeInternalRenderAreaMegapixels(targetAreaMegapixels) * MEGAPIXEL;
|
||||||
|
const areaScale = Math.sqrt(targetArea / (safeClientWidth * safeClientHeight));
|
||||||
|
const dimensionScale = Math.min(
|
||||||
|
areaScale,
|
||||||
|
getSafeMaxPixelScale(maxPixelScale),
|
||||||
|
safeMaxTextureDimension / safeClientWidth,
|
||||||
|
safeMaxTextureDimension / safeClientHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
height: Math.max(1, Math.round(safeClientHeight * dimensionScale)),
|
||||||
|
width: Math.max(1, Math.round(safeClientWidth * dimensionScale)),
|
||||||
|
};
|
||||||
|
};
|
||||||
34
src/pipelines/agents/agent-dispatch.ts
Normal file
34
src/pipelines/agents/agent-dispatch.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
export const AGENT_WORKGROUP_SIZE = 64;
|
||||||
|
export const AGENT_MAX_DISPATCH_WORKGROUPS_PER_DIMENSION = 65_535;
|
||||||
|
export const AGENT_MAX_DISPATCHABLE_COUNT = 0xffffffff;
|
||||||
|
|
||||||
|
export const getAgentDispatchWorkgroups = (agentCount: number): [number, number] => {
|
||||||
|
if (!Number.isFinite(agentCount) || agentCount <= 0) {
|
||||||
|
throw new Error('Agent count must be a positive finite number');
|
||||||
|
}
|
||||||
|
|
||||||
|
const workgroupCount = Math.ceil(agentCount / AGENT_WORKGROUP_SIZE);
|
||||||
|
if (workgroupCount <= AGENT_MAX_DISPATCH_WORKGROUPS_PER_DIMENSION) {
|
||||||
|
return [workgroupCount, 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const workgroupX = Math.min(
|
||||||
|
Math.ceil(Math.sqrt(workgroupCount)),
|
||||||
|
AGENT_MAX_DISPATCH_WORKGROUPS_PER_DIMENSION
|
||||||
|
);
|
||||||
|
const workgroupY = Math.ceil(workgroupCount / workgroupX);
|
||||||
|
|
||||||
|
if (workgroupY > AGENT_MAX_DISPATCH_WORKGROUPS_PER_DIMENSION) {
|
||||||
|
throw new Error('Agent count exceeds dispatchable workgroup range');
|
||||||
|
}
|
||||||
|
|
||||||
|
return [workgroupX, workgroupY];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dispatchAgentWorkgroups = (
|
||||||
|
passEncoder: GPUComputePassEncoder,
|
||||||
|
agentCount: number
|
||||||
|
): void => {
|
||||||
|
const [workgroupX, workgroupY] = getAgentDispatchWorkgroups(agentCount);
|
||||||
|
passEncoder.dispatchWorkgroups(workgroupX, workgroupY);
|
||||||
|
};
|
||||||
12
src/utils/hex-to-rgb.ts
Normal file
12
src/utils/hex-to-rgb.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
const HEX_COLOR_PATTERN =
|
||||||
|
/^#?(?<red>[0-9a-f]{2})(?<green>[0-9a-f]{2})(?<blue>[0-9a-f]{2})$/i;
|
||||||
|
|
||||||
|
export const hexToRgb = (hex: string): [number, number, number] => {
|
||||||
|
const match = HEX_COLOR_PATTERN.exec(hex);
|
||||||
|
if (!match?.groups) {
|
||||||
|
return [0, 0, 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { red, green, blue } = match.groups;
|
||||||
|
return [parseInt(red, 16) / 255, parseInt(green, 16) / 255, parseInt(blue, 16) / 255];
|
||||||
|
};
|
||||||
29
src/utils/math.ts
Normal file
29
src/utils/math.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
export const clamp = (value: number, min: number, max: number): number =>
|
||||||
|
Math.min(max, Math.max(min, value));
|
||||||
|
|
||||||
|
export const clamp01 = (value: number): number => clamp(value, 0, 1);
|
||||||
|
|
||||||
|
export const mix = (from: number, to: number, amount: number): number =>
|
||||||
|
from + (to - from) * amount;
|
||||||
|
|
||||||
|
export const mixAngle = (from: number, to: number, amount: number): number => {
|
||||||
|
const delta = Math.atan2(Math.sin(to - from), Math.cos(to - from));
|
||||||
|
return from + delta * amount;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const approach = (
|
||||||
|
current: number,
|
||||||
|
target: number,
|
||||||
|
elapsedSeconds: number,
|
||||||
|
timeConstantSeconds: number
|
||||||
|
): number => {
|
||||||
|
const amount = 1 - Math.exp(-elapsedSeconds / Math.max(0.001, timeConstantSeconds));
|
||||||
|
return mix(current, target, amount);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const smoothstep = (edge0: number, edge1: number, value: number): number => {
|
||||||
|
const amount = clamp01((value - edge0) / (edge1 - edge0));
|
||||||
|
return amount * amount * (3 - 2 * amount);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const easeOutQuad = (value: number): number => value * (2 - value);
|
||||||
Loading…
Reference in a new issue