more clean up
This commit is contained in:
parent
15e99380b5
commit
ea0304356f
11 changed files with 710 additions and 0 deletions
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…
Add table
Add a link
Reference in a new issue