From f300dbd394a76739005e206cd7c49c4e5a6690da Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Fri, 22 May 2026 07:54:38 +0100 Subject: [PATCH 1/5] Getting there --- e2e/app.spec.ts | 16 + package.json | 4 +- public/og-image.jpg | Bin 27787 -> 308311 bytes src/audio/garden-audio-config.ts | 10 +- src/audio/garden-audio-graph.ts | 19 +- src/audio/garden-audio-music.ts | 16 +- src/audio/garden-audio.ts | 20 +- src/audio/generative-piano-tuning.ts | 12 + src/audio/generative-piano.ts | 68 ++- src/config.ts | 5 +- src/config/brush-size.test.ts | 27 ++ src/config/brush-size.ts | 19 + src/config/color-interactions.ts | 12 - src/config/default-settings.ts | 10 +- src/config/runtime-controls.ts | 24 +- src/config/types.ts | 20 +- src/config/vibe-presets.test.ts | 66 +++ src/config/vibe-presets.ts | 423 +++++++++++------- src/game-loop/agent-population.ts | 7 +- src/game-loop/brush-stroke-smoother.ts | 7 +- src/game-loop/game-loop-resources.ts | 8 +- src/game-loop/simulation-frame.ts | 19 +- src/game-loop/simulation-textures.ts | 41 +- src/index.ts | 4 +- src/page/config-pane.ts | 6 +- src/page/vibe-navigator.ts | 41 +- src/pipelines/agents/agent-dispatch.ts | 40 +- .../agent-generation-pipeline.ts | 23 +- src/pipelines/agents/agent-limits.ts | 4 +- src/pipelines/agents/agent-pipeline.ts | 102 +++-- src/pipelines/agents/agent.wgsl | 277 +++++++----- src/pipelines/brush/brush-pipeline.ts | 8 +- src/pipelines/diffusion/diffuse.wgsl | 7 +- src/pipelines/diffusion/diffusion-pipeline.ts | 66 ++- src/pipelines/eraser/eraser-agent-pipeline.ts | 4 +- src/settings.ts | 6 +- src/style/_config-pane.scss | 4 + src/style/toolbar/_layout.scss | 12 +- src/style/toolbar/_responsive.scss | 3 +- src/vibe-uri.test.ts | 54 +++ src/vibe-uri.ts | 148 ++++++ src/vibes.ts | 19 +- vite.config.ts | 1 - 43 files changed, 1218 insertions(+), 464 deletions(-) create mode 100644 src/config/brush-size.test.ts create mode 100644 src/config/brush-size.ts create mode 100644 src/config/vibe-presets.test.ts create mode 100644 src/vibe-uri.test.ts create mode 100644 src/vibe-uri.ts diff --git a/e2e/app.spec.ts b/e2e/app.spec.ts index 19f0efd..6f50cb8 100644 --- a/e2e/app.spec.ts +++ b/e2e/app.spec.ts @@ -138,6 +138,22 @@ test('shows a clear fallback when WebGPU is unavailable', async ({ page }) => { expect(browserFailures).toEqual([]); }); +test('syncs the selected vibe with the URI', async ({ page }) => { + const browserFailures = collectLocalBrowserFailures(page); + + await disableWebGpu(page); + await page.goto('/?vibe=Bone%20Archive'); + + await expect(page).toHaveURL(/vibe=bone-archive/); + + await page.locator('.next-vibe').click(); + await expect(page).toHaveURL(/vibe=pelagic-caustics/); + + await page.goBack(); + await expect(page).toHaveURL(/vibe=bone-archive/); + expect(browserFailures).toEqual([]); +}); + test('keeps audio focus outlines scoped to the active control', async ({ page }) => { await disableWebGpu(page); await page.goto('/'); diff --git a/package.json b/package.json index b8cbfec..9d07a0f 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "@eslint/js": "^10.0.1", "@ianvs/prettier-plugin-sort-imports": "^4.7.1", "@playwright/test": "^1.60.0", - "@tweakpane/core": "^2.0.5", + "@tweakpane/core": "~2.0.5", "@types/node": "^25.6.0", "@vite-pwa/assets-generator": "^1.0.2", "@vitejs/plugin-basic-ssl": "^2.3.0", @@ -71,6 +71,6 @@ }, "dependencies": { "@plausible-analytics/tracker": "^0.4.5", - "tweakpane": "^4.0.5" + "tweakpane": "~4.0.5" } } diff --git a/public/og-image.jpg b/public/og-image.jpg index 03c89396f7d990d6d20194579053a97f2b723bfa..0ced8fc93d20464518405d46eb0982d01265d314 100644 GIT binary patch literal 308311 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 z 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 zĺ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%0D(px*I}`}%;wh6thH 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 z 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/Su&Or+{7RC`W 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≻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�HFpy z05pQm#0`lkC8K%$#m%&liY}i~DXmd(q`s=Ntgn#ckQofI7Z)0gI}*%!fr2@|5$r`8 z9y8YJ8@=@e#}IB^FKdKE79M7PqAQ&DVjM-+m=5t>`h`H`gatb?#?u{Uit zs`J!P5LhfS;+9r$`En1BWwy3)7E;F*25m+xp9z$bPL%ApyqW#JO)6HH+ z$_7*_c|?CpSpYy#NT8rlU(^EZTzZeVUWprg`rtvQuE3P3cMg({+(!8Ph*}nMAiZmx zsaE`}spd9s)U1BtF&yI4kW_p1JeT-_f-A78bt|inxKt`Yqwtrz0taB)sq|pq;_vYO z6ojGtR2xlpTMVg0v0mRG7Rtxe@i^GV9dUyBiRK~iJx>KuRe{Ut3vlUAsKG$UjLE${ zy{u_lCBv@3Z7MWs8+sariVup0N-kBx;sVhIRkym+vjuP9N8Fn5oZ*C`23V5i2?hD_ z5>Q)0!0z|_m#xAJC`D0zzM~yBb@2wkj}~4*E!mzdy_Lhr9$fL2RN3d-gGqJ|%EuF` z4)S9zj>ST%(JzG-3mfD{wp-du(B|aP6})4jrPRKV{cagr&^WgO5*1DR-}IgFNjQL8 zcM8}h$CWAP?(Ksj*=N+DS{j#Mt;M^Vi_pg;)$O@G?cn1C2rAOyT7teGxkQQ(3lgw- zV(noM9li(~&XxZFvEqm>0^VF+lliaW1)$gy%+c7?-TIkgY^r!l`f|r$;5B^hKndzA zauXygw#IB)6Km~$HUlkD`gvvOE*CV<^#BWC=VNcCs%p?nrg-^hU<(eAN0fIWs&hlc zq)$Ra2sTGV#3kF}2gj9eOCp4=T*+ciNeVw-gYi*f(JIzGnUf`=*|3ceY?QylXC*=M zjIFpA`0Q?Mw5X`~ekHEw#NliBGBixPCFR1O!&h)B;T}z%E14Yuh7m3`>CVyQl&M9u zeZZ)~SSc8RXP7N3q@x3UObweU5)P0Vi!D)MVQmec#L`7^@Y~6HOWQ^M<0}WL)G>JYhVVyFlM#<7q{g+!B+G5-w5^>e~&40*(j?b{8R9o z56d(0cLOin4*-{6+*+a$RBLbXglQvmeZ{N4c6~xgcRjmTC=9RnDO%|qheZ?)q4{hN zb&1R4CW%zq+9h@Qt0NDZucBGEowXTL%=8eXP$YRw$Iy6-EVKo<=bA59#5J8wv9h1F z?Z-E6EK~;kN3mA^97Es=30N27`HPB*52%adQE*iul(6`mtiV^qPrnd7H*{1AfK*Wd zr!L`xSB^Y`VvivdnpOvuZBbtxfd!o~eo(s+`pKU?^aDuHh;W zL65#MEOJ3fys)ly1Ka6C3?#0uCj&SmOXdh>0t%^te5Gxlvvi zbTLEp?D*bO08zxNf`MLkEsoP>%SM^*Dn5n~cp0+7brBZM_AsSF%Noqccm=KNC?X9j z@;pAFSkp)*_yqB%>hH`UEun5?7htq2>IHxgZh|036 g3ZfPF8V2C-{3eeP5%aL>Y7M`IsKK-U0D?3B*(H)JzW@LL literal 27787 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 Q&bO7@1pXre2j7hR2Ke<4IsgCw diff --git a/src/audio/garden-audio-config.ts b/src/audio/garden-audio-config.ts index d5c8d70..bc6b599 100644 --- a/src/audio/garden-audio-config.ts +++ b/src/audio/garden-audio-config.ts @@ -1,9 +1,11 @@ import { DEFAULT_AUDIO_VOLUME } from '../consts'; import type { PianoNoteRole } from './garden-audio-types'; +type GardenAudioChordQuality = 'major' | 'minor' | 'sus2' | 'sus4'; + export interface GardenAudioChord { rootOffset: number; - quality: 'major' | 'minor'; + quality: GardenAudioChordQuality; } export interface GardenAudioVibeSettings { @@ -14,6 +16,8 @@ export interface GardenAudioVibeSettings { noteLength: number; notePitchOffset: number; brightness: number; + scale?: Array; + progression?: Array; } export interface GardenAudioVibeProfile extends GardenAudioVibeSettings { @@ -37,7 +41,9 @@ export const createGardenAudioConfig = () => ({ fadeInSeconds: 0.45, updateRampSeconds: 0.08, delay: { - timeSeconds: 0.405, + timeBeats: 0.5, + timeMinSeconds: 0.18, + timeMaxSeconds: 0.72, feedback: 0.12, wetGain: 0.044, erasingActivity: 0.12, diff --git a/src/audio/garden-audio-graph.ts b/src/audio/garden-audio-graph.ts index 88010ea..d992b95 100644 --- a/src/audio/garden-audio-graph.ts +++ b/src/audio/garden-audio-graph.ts @@ -121,19 +121,19 @@ export class GardenAudioGraph { ); } - public applyDelayProfile(): void { + public applyDelayProfile(bpm: number): void { if (!this.context || !this.delayNode) { return; } this.delayNode.delayTime.setTargetAtTime( - this.config.delay.timeSeconds, + this.getDelayTimeSecondsForBpm(bpm), this.context.currentTime, this.config.delay.timeRampSeconds ); } - public updateDelay(activity: number): void { + public updateDelay(activity: number, bpm: number): void { if (!this.context || !this.delayNode || !this.delayFeedback || !this.delayOutput) { return; } @@ -141,7 +141,7 @@ export class GardenAudioGraph { const now = this.context.currentTime; const normalizedActivity = clamp(activity, 0, 1); this.delayNode.delayTime.setTargetAtTime( - this.config.delay.timeSeconds, + this.getDelayTimeSecondsForBpm(bpm), now, this.config.delay.timeRampSeconds ); @@ -214,7 +214,7 @@ export class GardenAudioGraph { const feedbackLowPass = context.createBiquadFilter(); const returnLowPass = context.createBiquadFilter(); - delayNode.delayTime.value = this.config.delay.timeSeconds; + delayNode.delayTime.value = this.getDelayTimeSecondsForBpm(this.config.rhythm.bpm); delayFeedback.gain.value = this.config.delay.feedback; delayOutput.gain.value = this.config.delay.wetGain; feedbackHighPass.type = 'highpass'; @@ -283,6 +283,15 @@ export class GardenAudioGraph { }); } + private getDelayTimeSecondsForBpm(bpm: number): number { + const safeBpm = Number.isFinite(bpm) ? Math.max(1, bpm) : this.config.rhythm.bpm; + return clamp( + (60 / safeBpm) * this.config.delay.timeBeats, + this.config.delay.timeMinSeconds, + this.config.delay.timeMaxSeconds + ); + } + private createNoiseBuffer(context: AudioContext): AudioBuffer { const buffer = context.createBuffer( 1, diff --git a/src/audio/garden-audio-music.ts b/src/audio/garden-audio-music.ts index 67be4bf..0d056e7 100644 --- a/src/audio/garden-audio-music.ts +++ b/src/audio/garden-audio-music.ts @@ -15,14 +15,24 @@ const DEFAULT_SCALE: ReadonlyArray = [0, 2, 4, 7, 9]; const profileCache = new WeakMap(); +const getProfileScale = (vibe: VibePreset): Array => { + const scale = vibe.audio.scale?.length ? vibe.audio.scale : DEFAULT_SCALE; + return [...scale]; +}; + +const getProfileProgression = (vibe: VibePreset): Array => + (vibe.audio.progression?.length ? vibe.audio.progression : DEFAULT_PROGRESSION).map( + (chord) => ({ ...chord }) + ); + export const getVibeProfile = (vibe: VibePreset): GardenAudioVibeProfile => { let profile = profileCache.get(vibe); if (!profile) { profile = { ...vibe.audio, rootMidi: DEFAULT_ROOT_MIDI + vibe.audio.notePitchOffset, - scale: DEFAULT_SCALE as Array, - progression: DEFAULT_PROGRESSION as Array, + scale: getProfileScale(vibe), + progression: getProfileProgression(vibe), }; profileCache.set(vibe, profile); return profile; @@ -30,5 +40,7 @@ export const getVibeProfile = (vibe: VibePreset): GardenAudioVibeProfile => { Object.assign(profile, vibe.audio); profile.rootMidi = DEFAULT_ROOT_MIDI + vibe.audio.notePitchOffset; + profile.scale = getProfileScale(vibe); + profile.progression = getProfileProgression(vibe); return profile; }; diff --git a/src/audio/garden-audio.ts b/src/audio/garden-audio.ts index fa92c10..5d6131c 100644 --- a/src/audio/garden-audio.ts +++ b/src/audio/garden-audio.ts @@ -1,7 +1,7 @@ import { ErrorHandler, Severity } from '../utils/error-handler'; import { clamp01 } from '../utils/math'; import type { VibeId, VibePreset } from '../vibes'; -import type { GardenAudioConfig } from './garden-audio-config'; +import type { GardenAudioConfig, GardenAudioVibeProfile } from './garden-audio-config'; import { GardenAudioEnergy } from './garden-audio-energy'; import { GardenAudioGestureState } from './garden-audio-gesture-state'; import { GardenAudioGraph } from './garden-audio-graph'; @@ -155,11 +155,12 @@ export class GardenAudio { ): void { this.lifecycle = 'started'; this.currentVibeId = vibe.id; - this.graph.applyDelayProfile(); + const profile = getVibeProfile(vibe); + this.graph.applyDelayProfile(profile.bpm); this.graph.setMasterGain(this.masterVolume, startupRampSeconds); if (cuePiano) { - this.pianoEngine.cue(context.currentTime, getVibeProfile(vibe)); + this.pianoEngine.cue(context.currentTime, profile); } } @@ -245,7 +246,7 @@ export class GardenAudio { if (!this.isGestureActive && this.isReleasingPiano) { this.updatePianoRelease(snapshot.vibe, context.currentTime); - this.updateDelay(snapshot); + this.updateDelay(snapshot, profile); return; } @@ -256,7 +257,7 @@ export class GardenAudio { ? this.config.eraser.pianoActivity : this.energy.getLevel(), }); - this.updateDelay(snapshot); + this.updateDelay(snapshot, profile); } public stroke(stroke: GardenAudioStroke): void { @@ -371,7 +372,10 @@ export class GardenAudio { } } - private updateDelay(snapshot: GardenAudioSnapshot): void { + private updateDelay( + snapshot: GardenAudioSnapshot, + profile: GardenAudioVibeProfile + ): void { const context = this.graph.context; if (!context) { return; @@ -380,7 +384,7 @@ export class GardenAudio { const activity = snapshot.isErasing ? this.config.delay.erasingActivity : this.energy.getLevel(); - this.graph.updateDelay(activity); + this.graph.updateDelay(activity, profile.bpm); } private applyVibe(vibe: VibePreset): void { @@ -390,7 +394,7 @@ export class GardenAudio { this.currentVibeId = vibe.id; const profile = getVibeProfile(vibe); - this.graph.applyDelayProfile(); + this.graph.applyDelayProfile(profile.bpm); this.pianoEngine.cue(this.graph.context.currentTime, profile); } } diff --git a/src/audio/generative-piano-tuning.ts b/src/audio/generative-piano-tuning.ts index 185cbee..e76d6f4 100644 --- a/src/audio/generative-piano-tuning.ts +++ b/src/audio/generative-piano-tuning.ts @@ -152,6 +152,12 @@ interface GenerativePianoTuning { min: number; max: number; }; + stereoWidth: { + idle: number; + active: number; + intense: number; + intenseThreshold: number; + }; stylePanOffsetScale: number; lowpass: { midiBase: number; @@ -371,6 +377,12 @@ export const generativePianoTuning: GenerativePianoTuning = { min: -3, max: 3, }, + stereoWidth: { + idle: 0.46, + active: 0.9, + intense: 1.16, + intenseThreshold: 0.72, + }, stylePanOffsetScale: 0.35, lowpass: { midiBase: 48, diff --git a/src/audio/generative-piano.ts b/src/audio/generative-piano.ts index e2505c5..5743964 100644 --- a/src/audio/generative-piano.ts +++ b/src/audio/generative-piano.ts @@ -18,23 +18,34 @@ import { PIANO_SCHEDULE_AHEAD_SECONDS } from './piano-sampler'; const GENERATIVE_LOOKAHEAD_SECONDS = 0.3; const GENERATIVE_START_DELAY_SECONDS = 0.02; -const chordVoicings = { - majorOpen: [0, 7, 12, 16], - minorOpen: [0, 7, 12, 15], - majorClosed: [0, 4, 7, 12, 16], - minorClosed: [0, 3, 7, 12, 15], +const chordVoicings: Record< + GardenAudioChord['quality'], + { closed: Array; open: Array } +> = { + major: { + closed: [0, 4, 7, 12, 16], + open: [0, 7, 12, 16], + }, + minor: { + closed: [0, 3, 7, 12, 15], + open: [0, 7, 12, 15], + }, + sus2: { + closed: [0, 2, 7, 12, 14], + open: [0, 7, 12, 14], + }, + sus4: { + closed: [0, 5, 7, 12, 17], + open: [0, 7, 12, 17], + }, }; const getChordIntervals = ( chord: GardenAudioChord, openVoicing: boolean ): Array => { - if (openVoicing) { - return chord.quality === 'major' ? chordVoicings.majorOpen : chordVoicings.minorOpen; - } - return chord.quality === 'major' - ? chordVoicings.majorClosed - : chordVoicings.minorClosed; + const voicing = chordVoicings[chord.quality]; + return openVoicing ? voicing.open : voicing.closed; }; const degreeToSemitone = (profile: GardenAudioVibeProfile, degree: number): number => { @@ -406,7 +417,7 @@ export class GenerativePianoEngine { velocity + expression * generativePianoTuning.padChord.expressionVelocityWeight, startTime, durationSeconds, - pan: register.pan, + pan: this.getActivityPan(register.pan, expression), role: 'pad', delaySend: generativePianoTuning.padChord.delaySend, lowpassHz: this.getLowpassHz( @@ -446,7 +457,7 @@ export class GenerativePianoEngine { velocity: release.velocities[index], startTime: startTime + index * release.strumSeconds, durationSeconds: release.durationSeconds, - pan: register.pan, + pan: this.getActivityPan(register.pan, 0), role: 'pad', delaySend: release.delaySend, lowpassHz: this.getLowpassHz(profile, midi, release.lowpassExpression), @@ -487,7 +498,7 @@ export class GenerativePianoEngine { durationSeconds: generativePianoTuning.supportNote.durationBaseSeconds + expression * generativePianoTuning.supportNote.durationExpressionSeconds, - pan: this.getStylePan(styleIndex), + pan: this.getStylePan(styleIndex, expression), role: 'support', delaySend: generativePianoTuning.supportNote.delaySendBase + @@ -533,7 +544,7 @@ export class GenerativePianoEngine { durationSeconds: generativePianoTuning.textureNote.durationBaseSeconds + expression * generativePianoTuning.textureNote.durationExpressionSeconds, - pan: this.getStylePan(styleIndex), + pan: this.getStylePan(styleIndex, expression), role: 'texture', delaySend: generativePianoTuning.textureNote.delaySendBase + @@ -582,7 +593,7 @@ export class GenerativePianoEngine { durationSeconds: generativePianoTuning.gestureAccent.durationBaseSeconds + strength * generativePianoTuning.gestureAccent.durationStrengthSeconds, - pan: this.getStylePan(styleIndex), + pan: this.getStylePan(styleIndex, strength), role: 'gesture', delaySend: generativePianoTuning.gestureAccent.delaySend, lowpassHz: this.getLowpassHz(profile, midi, strength), @@ -627,7 +638,7 @@ export class GenerativePianoEngine { durationSeconds: generativePianoTuning.touchNote.durationBaseSeconds + strength * generativePianoTuning.touchNote.durationStrengthSeconds, - pan: this.getStylePan(styleIndex), + pan: this.getStylePan(styleIndex, strength), role: 'gesture', delaySend: generativePianoTuning.touchNote.delaySend, lowpassHz: this.getLowpassHz( @@ -813,7 +824,7 @@ export class GenerativePianoEngine { chordOffsets: this.getChordOffsets(chord, chordIntervals), }; const midi = this.chooseMidi(source, register, this.lastBrushStreamMidi, true); - const pan = this.getStylePan(styleIndex); + const pan = this.getStylePan(styleIndex, intensity); const durationSeconds = clamp( generativePianoTuning.brushStream.durationBaseSeconds + intensity * generativePianoTuning.brushStream.durationIntensitySeconds - @@ -1128,16 +1139,29 @@ export class GenerativePianoEngine { styleCount) as GardenAudioStyleIndex; } - private getStylePan(styleIndex: GardenAudioStyleIndex): number { + private getStylePan(styleIndex: GardenAudioStyleIndex, activity: number): number { const pool = generativePianoTuning.stylePools[styleIndex]; const styleVoice = styleVoices[styleIndex]; - return clamp( + return this.getActivityPan( pool.pan + styleVoice.panOffset * generativePianoTuning.stylePanOffsetScale, - -1, - 1 + activity ); } + private getActivityPan(pan: number, activity: number): number { + const { active, idle, intense, intenseThreshold } = generativePianoTuning.stereoWidth; + const normalizedActivity = clamp01(activity); + const safeThreshold = clamp(intenseThreshold, 0.001, 0.999); + const width = + normalizedActivity < safeThreshold + ? idle + ((active - idle) * normalizedActivity) / safeThreshold + : active + + ((intense - active) * (normalizedActivity - safeThreshold)) / + (1 - safeThreshold); + + return clamp(pan * width, -1, 1); + } + private getLowpassHz( profile: GardenAudioVibeProfile, midi: number, diff --git a/src/config.ts b/src/config.ts index d458f1e..473dbf8 100644 --- a/src/config.ts +++ b/src/config.ts @@ -107,8 +107,7 @@ export const appConfig = { titleStrokeWidthRatio: 0.11, verticalAnchor: 0.47, }, - introMoveSpeedBaseMultiplier: 1.8, - introMoveSpeedProgressMultiplier: 0.35, + introMoveSpeed: 280, stroke: { densityMultiplier: 110, maxAgentCount: 2_400, @@ -129,7 +128,7 @@ export const appConfig = { step: 1, }, mirror: { - default: 1, + default: 8, fallbackSegmentName: 'slices', max: 12, min: 1, diff --git a/src/config/brush-size.test.ts b/src/config/brush-size.test.ts new file mode 100644 index 0000000..9039e62 --- /dev/null +++ b/src/config/brush-size.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest'; + +import { + BRUSH_SIZE_BASELINE_RENDER_AREA_MEGAPIXELS, + getBrushRenderQualityScale, + getRenderQualityBrushSize, +} from './brush-size'; + +describe('render-quality brush sizing', () => { + it('keeps brush sizes unchanged at the 7.3 MP baseline', () => { + expect( + getRenderQualityBrushSize(21, BRUSH_SIZE_BASELINE_RENDER_AREA_MEGAPIXELS) + ).toBe(21); + }); + + it('scales linear brush size with the square root of render area', () => { + const doubledLinearQuality = BRUSH_SIZE_BASELINE_RENDER_AREA_MEGAPIXELS * 4; + + expect(getBrushRenderQualityScale(doubledLinearQuality)).toBe(2); + expect(getRenderQualityBrushSize(9.75, doubledLinearQuality)).toBe(19.5); + }); + + it('falls back to baseline scaling for invalid render areas', () => { + expect(getBrushRenderQualityScale(0)).toBe(1); + expect(getRenderQualityBrushSize(6.5, Number.NaN)).toBe(6.5); + }); +}); diff --git a/src/config/brush-size.ts b/src/config/brush-size.ts new file mode 100644 index 0000000..fc77697 --- /dev/null +++ b/src/config/brush-size.ts @@ -0,0 +1,19 @@ +export const BRUSH_SIZE_BASELINE_RENDER_AREA_MEGAPIXELS = 7.3; + +const getSafeRenderAreaMegapixels = (renderAreaMegapixels: number): number => + Number.isFinite(renderAreaMegapixels) && renderAreaMegapixels > 0 + ? renderAreaMegapixels + : BRUSH_SIZE_BASELINE_RENDER_AREA_MEGAPIXELS; + +export const getBrushRenderQualityScale = (renderAreaMegapixels: number): number => + Math.sqrt( + getSafeRenderAreaMegapixels(renderAreaMegapixels) / + BRUSH_SIZE_BASELINE_RENDER_AREA_MEGAPIXELS + ); + +export const getRenderQualityBrushSize = ( + brushSize: number, + renderAreaMegapixels: number +): number => + Math.max(0, Number.isFinite(brushSize) ? brushSize : 0) * + getBrushRenderQualityScale(renderAreaMegapixels); diff --git a/src/config/color-interactions.ts b/src/config/color-interactions.ts index 4e3ebc6..c84e61d 100644 --- a/src/config/color-interactions.ts +++ b/src/config/color-interactions.ts @@ -1,17 +1,5 @@ import type { NumberControlConfig } from './types'; -export const colorInteractionSettings = { - color1ToColor1: 1, - color1ToColor2: 0, - color1ToColor3: 0, - color2ToColor1: 0, - color2ToColor2: 1, - color2ToColor3: 0, - color3ToColor1: 0, - color3ToColor2: 0, - color3ToColor3: 1, -}; - export const colorInteractionControl = (label: string): NumberControlConfig => ({ folder: 'Color Reactions', label, diff --git a/src/config/default-settings.ts b/src/config/default-settings.ts index 033a723..bdcf049 100644 --- a/src/config/default-settings.ts +++ b/src/config/default-settings.ts @@ -1,4 +1,3 @@ -import { colorInteractionSettings } from './color-interactions'; import { runtimeControls } from './runtime-controls'; import type { GardenAppConfig } from './types'; @@ -27,12 +26,8 @@ const computeDefaultInternalRenderAreaMegapixels = (): number => { }; export const defaultSettings: GardenAppConfig['defaultSettings'] = { - ...colorInteractionSettings, selectedColorIndex: 0, - turnWhenLost: 0.8, - forwardRotationScale: 0.25, - sensorOffsetAngle: 32, introNearDistanceMin: 28, introNearDistanceInner: 4, introNearSensorOffsetMultiplier: 0.75, @@ -40,8 +35,6 @@ export const defaultSettings: GardenAppConfig['defaultSettings'] = { introProgressCutoff: 0.999, introTurnRateMultiplier: 3.4, introRandomTurnMultiplier: 0.18, - introFarMoveMultiplier: 2.65, - introNearMoveMultiplier: 0.01, introStepStopDistance: 0.5, randomTimeScale: 0.34816, @@ -58,7 +51,6 @@ export const defaultSettings: GardenAppConfig['defaultSettings'] = { brushCurveMirrorResolutionExponent: 0.5, brushCurveSegmentBrushRadiusRatio: 0.65, brushSmoothingMinSampleDistance: 0.5, - strokeAngleJitterRadians: Math.PI * 0.7, brushAlpha: 1, brushDiscardThreshold: 0.02, @@ -78,7 +70,7 @@ export const defaultSettings: GardenAppConfig['defaultSettings'] = { adaptiveCapInitial: 1_000_000, adaptiveCapMin: 50_000, internalRenderAreaMegapixels: computeDefaultInternalRenderAreaMegapixels(), - maxAgentCount: 700_000, + maxAgentCount: 1_500_000, renderTraceNormalizationFloor: 1, renderBrushColorBase: 1.2, diff --git a/src/config/runtime-controls.ts b/src/config/runtime-controls.ts index a839746..82278b0 100644 --- a/src/config/runtime-controls.ts +++ b/src/config/runtime-controls.ts @@ -4,6 +4,16 @@ import type { GardenAppConfig } from './types'; const formatPercent = (value: number): string => `${Math.round(value * 100)}%`; const formatRadiansAsDegrees = (value: number): string => `${Math.round((value * 180) / Math.PI)} deg`; +const formatCompactNumber = (value: number): string => { + if (value >= 1_000_000) { + const millions = value / 1_000_000; + return `${Number.isInteger(millions) ? millions : millions.toFixed(1)}M`; + } + if (value >= 1_000) { + return `${Math.round(value / 1_000)}k`; + } + return `${value}`; +}; export const runtimeControls: GardenAppConfig['runtimeSettings']['controls'] = { color1ToColor1: colorInteractionControl('Color 1 Follows Color 1'), @@ -20,14 +30,14 @@ export const runtimeControls: GardenAppConfig['runtimeSettings']['controls'] = { folder: 'Brush', label: 'Brush Size', min: 1, - max: 60, + max: 36, step: 0.25, }, spawnPerPixel: { folder: 'Brush', label: 'Density', min: 0.01, - max: 1, + max: 0.38, step: 0.001, }, strokeAngleJitterRadians: { @@ -35,7 +45,7 @@ export const runtimeControls: GardenAppConfig['runtimeSettings']['controls'] = { format: formatRadiansAsDegrees, label: 'Spawn Spread', min: 0, - max: Math.PI * 2, + max: Math.PI, step: 0.01, }, sensorOffsetDistance: { @@ -81,6 +91,13 @@ export const runtimeControls: GardenAppConfig['runtimeSettings']['controls'] = { max: 1, step: 0.001, }, + diffusionRateTrails: { + folder: 'Movement', + label: 'Diffusion Rate', + min: 0.01, + max: 1, + step: 0.01, + }, decayRateTrails: { folder: 'Movement', label: 'Trail Fade', @@ -106,6 +123,7 @@ export const runtimeControls: GardenAppConfig['runtimeSettings']['controls'] = { maxAgentCount: { folder: 'Performance', + format: formatCompactNumber, integer: true, label: 'Population Limit', min: 0, diff --git a/src/config/types.ts b/src/config/types.ts index ad80e83..1c6bedf 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -52,17 +52,30 @@ type RuntimeSettingControlConfig = Partial< Record >; -type GardenVibeSettings = Pick< +export type GardenVibeSettings = Pick< GardenRuntimeSettings, | 'backgroundGrainStrength' | 'brushSize' | 'clarity' + | 'color1ToColor1' + | 'color1ToColor2' + | 'color1ToColor3' + | 'color2ToColor1' + | 'color2ToColor2' + | 'color2ToColor3' + | 'color3ToColor1' + | 'color3ToColor2' + | 'color3ToColor3' | 'decayRateTrails' + | 'forwardRotationScale' | 'individualTrailWeight' | 'moveSpeed' + | 'sensorOffsetAngle' | 'sensorOffsetDistance' | 'spawnPerPixel' + | 'strokeAngleJitterRadians' | 'turnSpeed' + | 'turnWhenLost' >; type GardenDefaultSettings = Omit< @@ -72,10 +85,8 @@ type GardenDefaultSettings = Omit< export enum VibeId { AuroraMycelium = 'aurora-mycelium', - EmberCircuit = 'ember-circuit', VelvetObservatory = 'velvet-observatory', LichenSignal = 'lichen-signal', - UltravioletSiren = 'ultraviolet-siren', TidepoolLantern = 'tidepool-lantern', PaperLanternFog = 'paper-lantern-fog', ChromePollen = 'chrome-pollen', @@ -179,8 +190,7 @@ export interface GardenAppConfig { titleStrokeWidthRatio: number; verticalAnchor: number; }; - introMoveSpeedBaseMultiplier: number; - introMoveSpeedProgressMultiplier: number; + introMoveSpeed: number; stroke: { densityMultiplier: number; maxAgentCount: number; diff --git a/src/config/vibe-presets.test.ts b/src/config/vibe-presets.test.ts new file mode 100644 index 0000000..4052166 --- /dev/null +++ b/src/config/vibe-presets.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it } from 'vitest'; + +import { vibePresets } from './vibe-presets'; + +const FINAL_VIBE_NAMES = [ + 'Aurora Mycelium', + 'Velvet Observatory', + 'Lichen Signal', + 'Tidepool Lantern', + 'Paper Lantern Fog Copy', + 'Chrome Pollen', +]; + +describe('vibePresets', () => { + it('keeps the classic preset set distinct', () => { + expect(vibePresets.map((preset) => preset.name)).toEqual(FINAL_VIBE_NAMES); + + const ids = vibePresets.map((preset) => preset.id); + expect(new Set(ids).size).toBe(vibePresets.length); + }); + + it('includes both blended and visibly particulate styles', () => { + const blendedNames = vibePresets + .filter( + (preset) => preset.settings.brushSize >= 17 && preset.settings.clarity <= 0.56 + ) + .map((preset) => preset.name); + const softParticleNames = vibePresets + .filter( + (preset) => preset.settings.brushSize <= 5 && preset.settings.clarity <= 0.2 + ) + .map((preset) => preset.name); + + expect(blendedNames).toEqual([ + 'Aurora Mycelium', + 'Tidepool Lantern', + ]); + expect(softParticleNames).toEqual(['Chrome Pollen']); + }); + + it('stays inside interactive performance guardrails', () => { + const violations = vibePresets.flatMap((preset) => { + const { name, settings } = preset; + const presetViolations: Array = []; + + if (settings.spawnPerPixel > 0.38) { + presetViolations.push(`${name} density exceeds 0.38`); + } + if (settings.brushSize > 36) { + presetViolations.push(`${name} brush size exceeds 36`); + } + if ( + settings.spawnPerPixel >= 0.28 && + (settings.decayRateTrails > 940 || + settings.brushSize > 14 || + settings.individualTrailWeight > 0.055) + ) { + presetViolations.push(`${name} combines high density with too much persistence`); + } + + return presetViolations; + }); + + expect(violations).toEqual([]); + }); +}); diff --git a/src/config/vibe-presets.ts b/src/config/vibe-presets.ts index a95a0a3..e25911a 100644 --- a/src/config/vibe-presets.ts +++ b/src/config/vibe-presets.ts @@ -1,5 +1,136 @@ -import { defaultGardenAudioVibeSettings } from '../audio/garden-audio-config'; -import { VibeId, type VibePreset } from './types'; +import { + defaultGardenAudioVibeSettings, + type GardenAudioChord, +} from '../audio/garden-audio-config'; +import { VibeId, type GardenVibeSettings, type VibePreset } from './types'; + +type ColorReactionSettings = Pick< + GardenVibeSettings, + | 'color1ToColor1' + | 'color1ToColor2' + | 'color1ToColor3' + | 'color2ToColor1' + | 'color2ToColor2' + | 'color2ToColor3' + | 'color3ToColor1' + | 'color3ToColor2' + | 'color3ToColor3' +>; + +const colorReactions = { + auroraMycelium: { + color1ToColor1: 1, + color1ToColor2: 1, + color1ToColor3: 0, + color2ToColor1: 0, + color2ToColor2: 1, + color2ToColor3: 1, + color3ToColor1: 1, + color3ToColor2: 0, + color3ToColor3: 1, + }, + velvetObservatory: { + color1ToColor1: 1, + color1ToColor2: -1, + color1ToColor3: -1, + color2ToColor1: -1, + color2ToColor2: 1, + color2ToColor3: -1, + color3ToColor1: -1, + color3ToColor2: -1, + color3ToColor3: 1, + }, + lichenSignal: { + color1ToColor1: 0, + color1ToColor2: -1, + color1ToColor3: 1, + color2ToColor1: -1, + color2ToColor2: 0, + color2ToColor3: -1, + color3ToColor1: 1, + color3ToColor2: -1, + color3ToColor3: 1, + }, + tidepoolLantern: { + color1ToColor1: 0, + color1ToColor2: 1, + color1ToColor3: 0, + color2ToColor1: 0, + color2ToColor2: 0, + color2ToColor3: 1, + color3ToColor1: 1, + color3ToColor2: 0, + color3ToColor3: 0, + }, + paperLanternFog: { + color1ToColor1: 1, + color1ToColor2: 1, + color1ToColor3: 1, + color2ToColor1: 1, + color2ToColor2: 1, + color2ToColor3: 1, + color3ToColor1: 1, + color3ToColor2: 1, + color3ToColor3: 1, + }, + chromePollen: { + color1ToColor1: 1, + color1ToColor2: 0, + color1ToColor3: 1, + color2ToColor1: -1, + color2ToColor2: 1, + color2ToColor3: 0, + color3ToColor1: 1, + color3ToColor2: 0, + color3ToColor3: 1, + }, +} satisfies Record; + +const musicScales = { + dorian: [0, 2, 3, 5, 7, 9, 10], + lydian: [0, 2, 4, 6, 7, 9, 11], + mixolydian: [0, 2, 4, 5, 7, 9, 10], + naturalMinor: [0, 2, 3, 5, 7, 8, 10], +} satisfies Record>; + +const musicProgressions = { + aurora: [ + { rootOffset: 0, quality: 'sus2' }, + { rootOffset: 7, quality: 'major' }, + { rootOffset: 9, quality: 'minor' }, + { rootOffset: 5, quality: 'sus4' }, + ], + chrome: [ + { rootOffset: 0, quality: 'major' }, + { rootOffset: 2, quality: 'major' }, + { rootOffset: 7, quality: 'sus2' }, + { rootOffset: 9, quality: 'minor' }, + ], + lichen: [ + { rootOffset: 0, quality: 'minor' }, + { rootOffset: 5, quality: 'major' }, + { rootOffset: 10, quality: 'major' }, + { rootOffset: 3, quality: 'major' }, + ], + paperLantern: [ + { rootOffset: 0, quality: 'minor' }, + { rootOffset: 8, quality: 'major' }, + { rootOffset: 5, quality: 'minor' }, + { rootOffset: 10, quality: 'sus4' }, + ], + tidepool: [ + { rootOffset: 0, quality: 'major' }, + { rootOffset: 10, quality: 'major' }, + { rootOffset: 5, quality: 'sus2' }, + { rootOffset: 9, quality: 'minor' }, + ], + velvet: [ + { rootOffset: 0, quality: 'minor' }, + { rootOffset: 8, quality: 'major' }, + { rootOffset: 3, quality: 'major' }, + { rootOffset: 5, quality: 'sus4' }, + ], +} satisfies Record>; export const defaultVibeId = VibeId.AuroraMycelium; @@ -14,15 +145,20 @@ export const vibePresets: Array = [ ], backgroundColor: [6, 13, 22], settings: { - backgroundGrainStrength: 0.016, - brushSize: 20, + ...colorReactions.auroraMycelium, + backgroundGrainStrength: 0.014, + brushSize: 21, clarity: 0.52, decayRateTrails: 988, - individualTrailWeight: 0.085, + forwardRotationScale: 0.28, + individualTrailWeight: 0.082, moveSpeed: 54, - sensorOffsetDistance: 72, - spawnPerPixel: 0.13, - turnSpeed: 35, + sensorOffsetAngle: 36, + sensorOffsetDistance: 76, + spawnPerPixel: 0.14, + strokeAngleJitterRadians: 1.45, + turnSpeed: 34, + turnWhenLost: 0.75, }, audio: { ...defaultGardenAudioVibeSettings, @@ -33,130 +169,84 @@ export const vibePresets: Array = [ noteLength: 0.86, notePitchOffset: -2, brightness: 0.84, - }, - }, - { - id: VibeId.EmberCircuit, - name: 'Ember Circuit', - colors: [ - [255, 95, 38], - [255, 43, 132], - [43, 219, 255], - ], - backgroundColor: [17, 10, 8], - settings: { - backgroundGrainStrength: 0.03, - brushSize: 8, - clarity: 0.82, - decayRateTrails: 918, - individualTrailWeight: 0.04, - moveSpeed: 150, - sensorOffsetDistance: 24, - spawnPerPixel: 0.31, - turnSpeed: 130, - }, - audio: { - ...defaultGardenAudioVibeSettings, - idleIntensity: 0.03, - bpm: 124, - rampUpIntensity: 1.35, - rampUpTime: 0.04, - noteLength: 0.18, - notePitchOffset: 7, - brightness: 1.34, + scale: musicScales.lydian, + progression: musicProgressions.aurora, }, }, { id: VibeId.VelvetObservatory, name: 'Velvet Observatory', colors: [ - [72, 98, 255], - [255, 89, 176], - [235, 236, 255], + [178, 76, 62], + [2, 174, 255], + [213, 193, 9], ], - backgroundColor: [7, 8, 20], + backgroundColor: [7, 4, 22], settings: { - backgroundGrainStrength: 0.01, - brushSize: 24, - clarity: 0.45, - decayRateTrails: 992, - individualTrailWeight: 0.095, - moveSpeed: 45, - sensorOffsetDistance: 86, - spawnPerPixel: 0.1, - turnSpeed: 24, + ...colorReactions.velvetObservatory, + backgroundGrainStrength: 0.005, + brushSize: 9.75, + clarity: 0.437, + decayRateTrails: 915, + forwardRotationScale: 2.0816681711721685e-17, + individualTrailWeight: 0.1, + moveSpeed: 216, + sensorOffsetAngle: 24, + sensorOffsetDistance: 17, + spawnPerPixel: 0.24, + strokeAngleJitterRadians: 0.16999999999999993, + turnSpeed: 33, + turnWhenLost: 0.42000000000000004, }, audio: { ...defaultGardenAudioVibeSettings, - idleIntensity: 0.14, - bpm: 56, - rampUpIntensity: 0.6, - rampUpTime: 0.16, - noteLength: 1.15, - notePitchOffset: -5, - brightness: 0.72, + idleIntensity: 0.55, + bpm: 72, + rampUpIntensity: 1.42, + rampUpTime: 0.07000000000000002, + noteLength: 0.7000000000000001, + notePitchOffset: 0, + brightness: 0.94, + scale: musicScales.naturalMinor, + progression: musicProgressions.velvet, }, }, { id: VibeId.LichenSignal, name: 'Lichen Signal', colors: [ - [174, 205, 91], - [71, 162, 126], - [229, 117, 71], + [183, 216, 92], + [65, 166, 128], + [238, 120, 76], ], - backgroundColor: [18, 24, 17], - settings: { - backgroundGrainStrength: 0.028, - brushSize: 17, - clarity: 0.66, - decayRateTrails: 974, - individualTrailWeight: 0.065, - moveSpeed: 68, - sensorOffsetDistance: 52, - spawnPerPixel: 0.19, - turnSpeed: 38, - }, - audio: { - ...defaultGardenAudioVibeSettings, - idleIntensity: 0.1, - bpm: 68, - rampUpIntensity: 0.8, - rampUpTime: 0.1, - noteLength: 0.62, - notePitchOffset: -3, - brightness: 0.82, - }, - }, - { - id: VibeId.UltravioletSiren, - name: 'Ultraviolet Siren', - colors: [ - [184, 75, 255], - [0, 224, 255], - [214, 255, 72], - ], - backgroundColor: [13, 9, 31], + backgroundColor: [0, 0, 0], settings: { + ...colorReactions.lichenSignal, backgroundGrainStrength: 0.02, - brushSize: 11, - clarity: 0.72, - decayRateTrails: 946, - individualTrailWeight: 0.052, - moveSpeed: 118, - sensorOffsetDistance: 30, - spawnPerPixel: 0.28, - turnSpeed: 96, + brushSize: 6.5, + clarity: 0.74, + decayRateTrails: 962, + forwardRotationScale: 0.3, + individualTrailWeight: 0.052000000000000005, + moveSpeed: 72, + sensorOffsetAngle: 42, + sensorOffsetDistance: 54, + spawnPerPixel: 0.15999999999999998, + strokeAngleJitterRadians: 3.1399999999999997, + turnSpeed: 44, + turnWhenLost: 0.9200000000000002, }, audio: { ...defaultGardenAudioVibeSettings, - idleIntensity: 0.04, - bpm: 112, - rampUpIntensity: 1.2, - rampUpTime: 0.05, - noteLength: 0.25, - notePitchOffset: 5, - brightness: 1.22, + idleIntensity: 0.13, + bpm: 68, + rampUpIntensity: 1.46, + rampUpTime: 0.10000000000000002, + noteLength: 0.6, + notePitchOffset: -3, + brightness: 1.21, + scale: musicScales.dorian, + progression: musicProgressions.lichen, }, }, { @@ -167,89 +257,110 @@ export const vibePresets: Array = [ [61, 118, 255], [255, 191, 91], ], - backgroundColor: [5, 20, 28], + backgroundColor: [4, 18, 29], settings: { + ...colorReactions.tidepoolLantern, backgroundGrainStrength: 0.018, - brushSize: 15, - clarity: 0.6, - decayRateTrails: 963, - individualTrailWeight: 0.058, + brushSize: 17, + clarity: 0.56, + decayRateTrails: 968, + forwardRotationScale: 0.38, + individualTrailWeight: 0.06, moveSpeed: 88, - sensorOffsetDistance: 44, + sensorOffsetAngle: 64, + sensorOffsetDistance: 46, spawnPerPixel: 0.22, - turnSpeed: 60, + strokeAngleJitterRadians: 1.8, + turnSpeed: 66, + turnWhenLost: 1.05, }, audio: { ...defaultGardenAudioVibeSettings, idleIntensity: 0.08, - bpm: 82, + bpm: 84, rampUpIntensity: 0.95, rampUpTime: 0.08, - noteLength: 0.48, + noteLength: 0.46, notePitchOffset: 0, brightness: 0.98, + scale: musicScales.mixolydian, + progression: musicProgressions.tidepool, }, }, { id: VibeId.PaperLanternFog, - name: 'Paper Lantern Fog', + name: 'Paper Lantern Fog Copy', colors: [ - [255, 174, 104], - [242, 102, 107], - [132, 211, 185], + [255, 176, 108], + [239, 90, 108], + [128, 213, 184], ], - backgroundColor: [31, 23, 20], + backgroundColor: [30, 23, 20], settings: { - backgroundGrainStrength: 0.036, - brushSize: 22, - clarity: 0.5, - decayRateTrails: 984, - individualTrailWeight: 0.08, - moveSpeed: 56, - sensorOffsetDistance: 64, - spawnPerPixel: 0.14, - turnSpeed: 32, + ...colorReactions.paperLanternFog, + backgroundGrainStrength: 0.038, + brushSize: 3.5, + clarity: 1, + decayRateTrails: 999, + forwardRotationScale: 0.24, + individualTrailWeight: 0.937, + moveSpeed: 28, + sensorOffsetAngle: 34, + sensorOffsetDistance: 66, + spawnPerPixel: 0.05499999999999998, + strokeAngleJitterRadians: 0, + turnSpeed: 30, + turnWhenLost: 1.52, }, audio: { ...defaultGardenAudioVibeSettings, - idleIntensity: 0.13, - bpm: 64, - rampUpIntensity: 0.72, - rampUpTime: 0.12, - noteLength: 0.9, - notePitchOffset: -4, - brightness: 0.76, + idleIntensity: 0.33, + bpm: 127, + rampUpIntensity: 0.66, + rampUpTime: 0.03000000000000001, + noteLength: 0.92, + notePitchOffset: 10, + brightness: 1.42, + scale: musicScales.naturalMinor, + progression: musicProgressions.paperLantern, }, }, { id: VibeId.ChromePollen, name: 'Chrome Pollen', colors: [ - [235, 255, 238], + [178, 34, 34], [255, 214, 48], [77, 240, 157], ], - backgroundColor: [9, 13, 12], + backgroundColor: [7, 12, 11], settings: { + ...colorReactions.chromePollen, backgroundGrainStrength: 0.012, - brushSize: 10, - clarity: 0.9, - decayRateTrails: 935, - individualTrailWeight: 0.045, - moveSpeed: 104, - sensorOffsetDistance: 36, - spawnPerPixel: 0.24, - turnSpeed: 78, + brushSize: 4.5, + clarity: 0.1, + decayRateTrails: 922, + forwardRotationScale: 0.5, + individualTrailWeight: 0.026000000000000002, + moveSpeed: 86, + sensorOffsetAngle: 46, + sensorOffsetDistance: 14, + spawnPerPixel: 0.36, + strokeAngleJitterRadians: 3, + turnSpeed: 34, + turnWhenLost: 1.35, }, audio: { ...defaultGardenAudioVibeSettings, - idleIntensity: 0.05, - bpm: 96, - rampUpIntensity: 1.05, - rampUpTime: 0.07, - noteLength: 0.3, - notePitchOffset: 3, - brightness: 1.18, + idleIntensity: 0.11, + bpm: 150, + rampUpIntensity: 2, + rampUpTime: 0.06000000000000001, + noteLength: 1.8, + notePitchOffset: -12, + brightness: 0.5, + scale: musicScales.lydian, + progression: musicProgressions.chrome, }, }, ]; diff --git a/src/game-loop/agent-population.ts b/src/game-loop/agent-population.ts index c00d7c7..68e05c0 100644 --- a/src/game-loop/agent-population.ts +++ b/src/game-loop/agent-population.ts @@ -1,6 +1,7 @@ import { vec2 } from 'gl-matrix'; import { appConfig } from '../config'; +import { getRenderQualityBrushSize } from '../config/brush-size'; import { AgentGenerationPipeline } from '../pipelines/agents/agent-generation/agent-generation-pipeline'; import { AGENT_FLOAT_COUNT, writeAgentValues } from '../pipelines/agents/agent-limits'; import { getSafePixelRatio } from '../pipelines/brush/brush-pipeline'; @@ -162,7 +163,11 @@ export class AgentPopulation { } const baseAngle = Math.atan2(deltaY, deltaX); - const spread = settings.brushSize * getSafePixelRatio(this.getCanvasPixelRatio()); + const spread = + getRenderQualityBrushSize( + settings.brushSize, + settings.internalRenderAreaMegapixels + ) * getSafePixelRatio(this.getCanvasPixelRatio()); const batchCapacity = this.strokeAgentData.length / AGENT_FLOAT_COUNT; if (batchCapacity <= 0) { return; diff --git a/src/game-loop/brush-stroke-smoother.ts b/src/game-loop/brush-stroke-smoother.ts index 80dd4b9..3d45283 100644 --- a/src/game-loop/brush-stroke-smoother.ts +++ b/src/game-loop/brush-stroke-smoother.ts @@ -1,6 +1,7 @@ import { vec2 } from 'gl-matrix'; import { appConfig } from '../config'; +import { getRenderQualityBrushSize } from '../config/brush-size'; import { getSafePixelRatio } from '../pipelines/brush/brush-pipeline'; import { settings } from '../settings'; import { type StrokeSegment } from './game-loop-types'; @@ -90,9 +91,13 @@ export class BrushStrokeSmoother { ): Array { const curveLength = vec2.distance(start, control) + vec2.distance(control, end); const canvasPixelRatio = getSafePixelRatio(this.options.getCanvasPixelRatio()); + const brushSize = getRenderQualityBrushSize( + settings.brushSize, + settings.internalRenderAreaMegapixels + ); const brushRadius = Math.max( settings.brushCurveMinBrushRadius * canvasPixelRatio, - (settings.brushSize * canvasPixelRatio) / 2 + (brushSize * canvasPixelRatio) / 2 ); const segmentSpacing = Math.max( settings.brushCurveMinSegmentSpacing * canvasPixelRatio, diff --git a/src/game-loop/game-loop-resources.ts b/src/game-loop/game-loop-resources.ts index 492204f..14588f9 100644 --- a/src/game-loop/game-loop-resources.ts +++ b/src/game-loop/game-loop-resources.ts @@ -137,12 +137,8 @@ export class GameLoopResources { deltaTime, time, agentCount: activeAgentCount, - moveSpeed: - settings.moveSpeed * - (introProgress >= 1 - ? 1 - : appConfig.simulation.introMoveSpeedBaseMultiplier + - introProgress * appConfig.simulation.introMoveSpeedProgressMultiplier), + moveSpeed: settings.moveSpeed, + introMoveSpeed: appConfig.simulation.introMoveSpeed, introProgress, }); this.brushPipeline.setParameters({ diff --git a/src/game-loop/simulation-frame.ts b/src/game-loop/simulation-frame.ts index 538ed45..ec4b9e3 100644 --- a/src/game-loop/simulation-frame.ts +++ b/src/game-loop/simulation-frame.ts @@ -46,16 +46,19 @@ export class SimulationFrameRenderer { const commandEncoder = this.device.createCommandEncoder(); this.gpuProfiler?.beginFrame(); - this.textures.copyTrailMapAToB(commandEncoder); + // Clear the deposit map up-front so agents write fresh deposits each frame + // and diffuse sees only this frame's contributions added to trailMapA. + this.textures.clearDepositMap(commandEncoder); let wroteSourceMap = false; if (isErasing) { if (this.pipelines.eraserAgentPipeline.hasActiveMask()) { const eraserMask = this.textures.eraserMask.getTextureView(); + // Erase trailMapA directly — it's what agent and diffuse will read. this.pipelines.eraserTexturePipeline.executeCombined( commandEncoder, eraserMask, this.textures.sourceMapA.getTextureView(), - this.textures.trailMapB.getTextureView(), + this.textures.trailMapA.getTextureView(), this.gpuProfiler?.timestampWrites('eraserTexture') ); this.pipelines.eraserAgentPipeline.execute( @@ -86,19 +89,20 @@ export class SimulationFrameRenderer { this.pipelines.agentPipeline.execute( commandEncoder, this.textures.trailMapA.getTextureView(), - this.textures.trailMapB.getTextureView(), + this.textures.depositMap.getTextureView(), this.gpuProfiler?.timestampWrites('agent') ); this.pipelines.diffusionPipeline.execute( commandEncoder, - this.textures.trailMapB.getTextureView(), this.textures.trailMapA.getTextureView(), + this.textures.trailMapB.getTextureView(), this.textures.trailMapA.getSize(), + this.textures.depositMap.getTextureView(), this.gpuProfiler?.timestampWrites('trailDiffusion') ); const canvasTexture = this.pipelines.renderPipeline.execute( commandEncoder, - this.textures.trailMapA.getTextureView(), + this.textures.trailMapB.getTextureView(), this.textures.sourceMapA.getTextureView(), useSourceMap, this.gpuProfiler?.timestampWrites('render') @@ -111,6 +115,7 @@ export class SimulationFrameRenderer { this.textures.sourceMapA.getTextureView(), this.textures.sourceMapB.getTextureView(), this.textures.sourceMapB.getSize(), + null, this.gpuProfiler?.timestampWrites('sourceDiffusion') ); } @@ -118,6 +123,10 @@ export class SimulationFrameRenderer { this.device.queue.submit([commandEncoder.finish()]); afterGpuProfileSubmit?.(); canvasReadbackRequest?.afterSubmit(); + // After this frame's diffuse, trailMapB holds the fresh trail; swap so + // trailMapA is "current trail" again for the next frame and any external + // readers (e.g. export snapshot). + this.textures.swapTrailMaps(); if (useSourceMap) { this.textures.swapSourceMaps(); this.sourceActiveFramesRemaining -= 1; diff --git a/src/game-loop/simulation-textures.ts b/src/game-loop/simulation-textures.ts index 7a07aa0..7832469 100644 --- a/src/game-loop/simulation-textures.ts +++ b/src/game-loop/simulation-textures.ts @@ -8,10 +8,16 @@ import { } from '../utils/graphics/resizable-texture'; export class SimulationTextures { - public readonly trailMapA: ResizableTexture; - public readonly trailMapB: ResizableTexture; + // trailMapA holds the current trail (read by agent and diffuse). trailMapB + // receives the diffuse output; the two swap each frame so the freshly + // diffused texture becomes trailMapA for the next frame. + public trailMapA: ResizableTexture; + public trailMapB: ResizableTexture; + // Per-frame deposit accumulator: cleared each frame, written sparsely by + // agents, then read by diffuse alongside trailMapA. Replaces the previous + // full-resolution copyTrailMapAToB seed. + public readonly depositMap: ResizableTexture; public readonly eraserMask: ResizableTexture; - // A/B are swapped each frame to ping-pong the diffusion pass. public sourceMapA: ResizableTexture; public sourceMapB: ResizableTexture; @@ -21,6 +27,7 @@ export class SimulationTextures { ) { this.trailMapA = this.createTexture(canvasSize); this.trailMapB = this.createTexture(canvasSize); + this.depositMap = this.createTexture(canvasSize); this.sourceMapA = this.createTexture(canvasSize); this.sourceMapB = this.createTexture(canvasSize); this.eraserMask = this.createEraserMask(canvasSize); @@ -36,6 +43,7 @@ export class SimulationTextures { const resizes = [ this.trailMapA, this.trailMapB, + this.depositMap, this.sourceMapA, this.sourceMapB, this.eraserMask, @@ -67,6 +75,7 @@ export class SimulationTextures { [ this.trailMapA, this.trailMapB, + this.depositMap, this.sourceMapA, this.sourceMapB, this.eraserMask, @@ -86,14 +95,25 @@ export class SimulationTextures { this.device.queue.submit([commandEncoder.finish()]); } - public copyTrailMapAToB(commandEncoder: GPUCommandEncoder): void { - const size = this.trailMapA.getSize(); + public clearDepositMap(commandEncoder: GPUCommandEncoder): void { + // Hardware fast-clear via a render pass with loadOp 'clear' and an empty + // body. Cheaper than copyTextureToTexture and writes no actual color data + // on tile-based GPUs. + const passEncoder = commandEncoder.beginRenderPass({ + colorAttachments: [ + { + view: this.depositMap.getTextureView(), + clearValue: { r: 0, g: 0, b: 0, a: 0 }, + loadOp: 'clear', + storeOp: 'store', + }, + ], + }); + passEncoder.end(); + } - commandEncoder.copyTextureToTexture( - { texture: this.trailMapA.getTexture() }, - { texture: this.trailMapB.getTexture() }, - { width: size[0], height: size[1] } - ); + public swapTrailMaps(): void { + [this.trailMapA, this.trailMapB] = [this.trailMapB, this.trailMapA]; } public clearSourceMaps(commandEncoder: GPUCommandEncoder): void { @@ -119,6 +139,7 @@ export class SimulationTextures { public destroy(): void { this.trailMapA.destroy(); this.trailMapB.destroy(); + this.depositMap.destroy(); this.sourceMapA.destroy(); this.sourceMapB.destroy(); this.eraserMask.destroy(); diff --git a/src/index.ts b/src/index.ts index fa06833..db0425f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -120,12 +120,12 @@ const main = async () => { new FullScreenHandler(fullScreenButton, document.documentElement); new VibeNavigator({ - onChange: ({ vibeId, vibeName, source }) => { + onChange: ({ vibeId, vibeName, source, userGesture }) => { trackVibeChange({ vibeId, vibeName, source }); game?.onVibeChanged(); syncRuntimeUi(); configPane?.refresh(); - game?.playVibeChangeAudio(true); + game?.playVibeChangeAudio(userGesture); }, }); diff --git a/src/page/config-pane.ts b/src/page/config-pane.ts index 52bf872..030f631 100644 --- a/src/page/config-pane.ts +++ b/src/page/config-pane.ts @@ -20,7 +20,11 @@ type PaneContainer = Pick; type ColorReactionKey = (typeof colorReactionRows)[number]['keys'][number]; type RuntimeControlKey = keyof GardenRuntimeSettings & string; type VibeColorKey = 'color1' | 'color2' | 'color3' | 'backgroundColor'; -type VibeNumberKey = keyof GardenAudioVibeSettings; +type NumberPropertyKey = { + [Key in keyof T]-?: T[Key] extends number ? Key : never; +}[keyof T] & + string; +type VibeNumberKey = NumberPropertyKey; interface PaneState extends GardenAudioVibeSettings { backgroundColor: string; diff --git a/src/page/vibe-navigator.ts b/src/page/vibe-navigator.ts index 2e37feb..f784e78 100644 --- a/src/page/vibe-navigator.ts +++ b/src/page/vibe-navigator.ts @@ -1,9 +1,11 @@ -import { activeVibe, applyVibeSettings } from '../settings'; +import { activeVibe, applyVibeSettings, rememberActiveVibeSelection } from '../settings'; import { queryRequiredElement } from '../utils/dom'; -import { VIBE_PRESETS, type VibeId } from '../vibes'; +import { getCurrentUriVibeId, writeCurrentVibeUri } from '../vibe-uri'; +import { getVibeById, VIBE_PRESETS, type VibeId } from '../vibes'; interface VibeSelection { source: string; + userGesture: boolean; vibeId: VibeId; vibeName: string; } @@ -20,18 +22,51 @@ export class VibeNavigator { private readonly nextButton = queryRequiredElement('.next-vibe', HTMLButtonElement); public constructor(private readonly options: VibeNavigatorOptions) { + rememberActiveVibeSelection(); + writeCurrentVibeUri(activeVibe.id, 'replace'); + this.previousButton.addEventListener('click', () => this.select(-1, 'previous-button') ); this.nextButton.addEventListener('click', () => this.select(1, 'next-button')); + window.addEventListener('popstate', () => this.selectFromCurrentUri()); } private select(offset: number, source: string): void { const current = VIBE_PRESETS.findIndex((vibe) => vibe.id === activeVibe.id); + const currentIndex = current >= 0 ? current : 0; const vibe = - VIBE_PRESETS[(current + VIBE_PRESETS.length + offset) % VIBE_PRESETS.length]; + VIBE_PRESETS[(currentIndex + VIBE_PRESETS.length + offset) % VIBE_PRESETS.length]; const activePreset = applyVibeSettings(vibe); + writeCurrentVibeUri(activePreset.id, 'push'); + this.notifyChange(activePreset, source, true); + } + + private selectFromCurrentUri(): void { + const vibeId = getCurrentUriVibeId(); + if (!vibeId || vibeId === activeVibe.id) { + writeCurrentVibeUri(activeVibe.id, 'replace'); + return; + } + + const vibe = getVibeById(vibeId); + if (!vibe) { + writeCurrentVibeUri(activeVibe.id, 'replace'); + return; + } + + const activePreset = applyVibeSettings(vibe); + writeCurrentVibeUri(activePreset.id, 'replace'); + this.notifyChange(activePreset, 'uri-popstate', false); + } + + private notifyChange( + activePreset: typeof activeVibe, + source: string, + userGesture: boolean + ): void { this.options.onChange({ + userGesture, vibeId: activePreset.id, vibeName: activePreset.name, source, diff --git a/src/pipelines/agents/agent-dispatch.ts b/src/pipelines/agents/agent-dispatch.ts index a5f97ff..158d556 100644 --- a/src/pipelines/agents/agent-dispatch.ts +++ b/src/pipelines/agents/agent-dispatch.ts @@ -1,15 +1,43 @@ -// Use the device's max workgroup size so we get full SIMD/wave occupancy on -// hardware that supports more than the WebGPU minimum of 256. -export const getAgentWorkgroupSize = (device: GPUDevice): number => - device.limits.maxComputeInvocationsPerWorkgroup; +const AGENT_WORKGROUP_KINDS = ['simulation', 'eraser', 'resize', 'compaction'] as const; + +export type AgentWorkgroupKind = (typeof AGENT_WORKGROUP_KINDS)[number]; + +const AGENT_WORKGROUP_SIZE_TARGETS = { + // Keep shader-specific targets conservative. Using the device maximum can + // hurt occupancy and makes compaction's workgroup scan more expensive. + simulation: 256, + eraser: 256, + resize: 256, + compaction: 256, +} satisfies Record; + +export const getAgentWorkgroupSize = ( + device: GPUDevice, + kind: AgentWorkgroupKind = 'simulation' +): number => { + const deviceLimit = Math.max( + 1, + Math.floor( + Math.min( + device.limits.maxComputeInvocationsPerWorkgroup, + device.limits.maxComputeWorkgroupSizeX + ) + ) + ); + return Math.min(AGENT_WORKGROUP_SIZE_TARGETS[kind], deviceLimit); +}; + +export const getMinAgentWorkgroupSize = (device: GPUDevice): number => + Math.min(...AGENT_WORKGROUP_KINDS.map((kind) => getAgentWorkgroupSize(device, kind))); export const substituteAgentWorkgroupSize = ( device: GPUDevice, - shaderCode: string + shaderCode: string, + kind: AgentWorkgroupKind = 'simulation' ): string => shaderCode.replaceAll( '__AGENT_WORKGROUP_SIZE__', - String(getAgentWorkgroupSize(device)) + String(getAgentWorkgroupSize(device, kind)) ); export const dispatchAgentWorkgroups = ( diff --git a/src/pipelines/agents/agent-generation/agent-generation-pipeline.ts b/src/pipelines/agents/agent-generation/agent-generation-pipeline.ts index b747e02..9b62fbb 100644 --- a/src/pipelines/agents/agent-generation/agent-generation-pipeline.ts +++ b/src/pipelines/agents/agent-generation/agent-generation-pipeline.ts @@ -36,7 +36,8 @@ export class AgentGenerationPipeline { private readonly resizePipeline: GPUComputePipeline; private readonly compactionPipeline: GPUComputePipeline; private readonly clearCompactedTailPipeline: GPUComputePipeline; - private readonly workgroupSize: number; + private readonly resizeWorkgroupSize: number; + private readonly compactionWorkgroupSize: number; private activeAgentsBuffer: GPUBuffer; private inactiveAgentsBuffer: GPUBuffer; @@ -110,20 +111,26 @@ export class AgentGenerationPipeline { usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, }); - this.workgroupSize = getAgentWorkgroupSize(device); - const sizedSchema = substituteAgentWorkgroupSize(device, agentSchema); + this.resizeWorkgroupSize = getAgentWorkgroupSize(device, 'resize'); + this.compactionWorkgroupSize = getAgentWorkgroupSize(device, 'compaction'); + const resizeSchema = substituteAgentWorkgroupSize(device, agentSchema, 'resize'); + const compactionSchema = substituteAgentWorkgroupSize( + device, + agentSchema, + 'compaction' + ); this.resizePipeline = device.createComputePipeline({ layout: device.createPipelineLayout({ bindGroupLayouts: [emptyBindGroupLayout, this.bindGroupLayout], }), compute: { - module: smartCompile(device, sizedSchema, resizeShader), + module: smartCompile(device, resizeSchema, resizeShader), entryPoint: 'main', }, }); - const compactionModule = smartCompile(device, sizedSchema, compactionShader); + const compactionModule = smartCompile(device, compactionSchema, compactionShader); this.compactionPipeline = device.createComputePipeline({ layout: device.createPipelineLayout({ @@ -248,7 +255,7 @@ export class AgentGenerationPipeline { const passEncoder = commandEncoder.beginComputePass(); passEncoder.setPipeline(this.resizePipeline); passEncoder.setBindGroup(1, this.getBindGroup()); - dispatchAgentWorkgroups(passEncoder, this.workgroupSize, agentCount); + dispatchAgentWorkgroups(passEncoder, this.resizeWorkgroupSize, agentCount); passEncoder.end(); this.device.queue.submit([commandEncoder.finish()]); @@ -267,11 +274,11 @@ export class AgentGenerationPipeline { const passEncoder = commandEncoder.beginComputePass(); passEncoder.setPipeline(this.compactionPipeline); passEncoder.setBindGroup(1, this.getBindGroup()); - dispatchAgentWorkgroups(passEncoder, this.workgroupSize, agentCount); + dispatchAgentWorkgroups(passEncoder, this.compactionWorkgroupSize, agentCount); passEncoder.setPipeline(this.clearCompactedTailPipeline); dispatchAgentWorkgroups( passEncoder, - this.workgroupSize, + this.compactionWorkgroupSize, Math.ceil(agentCount / AgentGenerationPipeline.CLEAR_COMPACTED_TAIL_STRIDE) ); passEncoder.end(); diff --git a/src/pipelines/agents/agent-limits.ts b/src/pipelines/agents/agent-limits.ts index fdf379a..405b37d 100644 --- a/src/pipelines/agents/agent-limits.ts +++ b/src/pipelines/agents/agent-limits.ts @@ -1,4 +1,4 @@ -import { getAgentWorkgroupSize } from './agent-dispatch'; +import { getMinAgentWorkgroupSize } from './agent-dispatch'; export const AGENT_FLOAT_COUNT = 8; export const AGENT_SIZE_IN_BYTES = AGENT_FLOAT_COUNT * Float32Array.BYTES_PER_ELEMENT; @@ -58,7 +58,7 @@ export const getMaxSupportedAgentCount = ( Math.floor(device.limits.maxBufferSize / AGENT_SIZE_IN_BYTES), Math.floor(storageBufferBindingSize / AGENT_SIZE_IN_BYTES), Math.floor(device.limits.maxComputeWorkgroupsPerDimension) * - getAgentWorkgroupSize(device) + getMinAgentWorkgroupSize(device) ) ); }; diff --git a/src/pipelines/agents/agent-pipeline.ts b/src/pipelines/agents/agent-pipeline.ts index e647f77..4d4dd0d 100644 --- a/src/pipelines/agents/agent-pipeline.ts +++ b/src/pipelines/agents/agent-pipeline.ts @@ -38,19 +38,27 @@ export interface AgentSettings { introNearDistanceInner: number; introTurnRateMultiplier: number; introRandomTurnMultiplier: number; - introFarMoveMultiplier: number; - introNearMoveMultiplier: number; introStepStopDistance: number; randomTimeScale: number; } -const UNIFORM_COUNT = 30; +// The Settings struct in WGSL starts with a mat3x3 reactionMatrix. +// In uniform layout each of its 3 columns is stored as a vec3 padded to +// 16 bytes, so the matrix occupies floats [0..12] (with [3], [7], [11] unused +// padding). Remaining scalars pack tightly from float 12 onward. +const UNIFORM_COUNT = 32; +const REACTION_MATRIX_COL0 = 0; +const REACTION_MATRIX_COL1 = 4; +const REACTION_MATRIX_COL2 = 8; +const SCALAR_BASE = 12; export class AgentPipeline { private readonly bindGroupLayout: GPUBindGroupLayout; - private readonly pipeline: GPUComputePipeline; + private readonly pipelineFull: GPUComputePipeline; + private readonly pipelineSteady: GPUComputePipeline; private readonly uniforms: GPUBuffer; private readonly workgroupSize: number; + private useSteadyPipeline = false; private readonly uniformValues = new Float32Array(UNIFORM_COUNT); private readonly uniformUintValues = new Uint32Array(this.uniformValues.buffer); private readonly uniformCache = createCachedBufferWrite( @@ -104,23 +112,30 @@ export class AgentPipeline { ], }); - this.workgroupSize = getAgentWorkgroupSize(device); + this.workgroupSize = getAgentWorkgroupSize(device, 'simulation'); const shaderModule = smartCompile( device, CommonState.shaderCode, - substituteAgentWorkgroupSize(device, agentSchema), + substituteAgentWorkgroupSize(device, agentSchema, 'simulation'), shader ); const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [commonState.bindGroupLayout, this.bindGroupLayout], }); - this.pipeline = device.createComputePipeline({ + this.pipelineFull = device.createComputePipeline({ layout: pipelineLayout, compute: { module: shaderModule, entryPoint: 'main', }, }); + this.pipelineSteady = device.createComputePipeline({ + layout: pipelineLayout, + compute: { + module: shaderModule, + entryPoint: 'mainSteady', + }, + }); this.uniforms = device.createBuffer({ size: UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT, @@ -153,8 +168,7 @@ export class AgentPipeline { introProgressCutoff, introTurnRateMultiplier, introRandomTurnMultiplier, - introFarMoveMultiplier, - introNearMoveMultiplier, + introMoveSpeed, introStepStopDistance, randomTimeScale, time, @@ -164,40 +178,46 @@ export class AgentPipeline { deltaTime: number; time: number; agentCount: number; + introMoveSpeed: number; introProgress?: number; }) { this.agentCount = agentCount; - this.uniformValues[0] = moveSpeed * deltaTime; - this.uniformValues[1] = turnSpeed * deltaTime; + const resolvedIntroProgress = introProgress ?? 1; + // Once the intro target phase ends nothing reads intro fields again, so the + // steady-only pipeline can replace the full one for the rest of the session. + this.useSteadyPipeline = resolvedIntroProgress >= introProgressCutoff; + // Reaction matrix: column N holds the weights for source colorIndex == N. + this.uniformValues[REACTION_MATRIX_COL0] = color1ToColor1; + this.uniformValues[REACTION_MATRIX_COL0 + 1] = color1ToColor2; + this.uniformValues[REACTION_MATRIX_COL0 + 2] = color1ToColor3; + this.uniformValues[REACTION_MATRIX_COL1] = color2ToColor1; + this.uniformValues[REACTION_MATRIX_COL1 + 1] = color2ToColor2; + this.uniformValues[REACTION_MATRIX_COL1 + 2] = color2ToColor3; + this.uniformValues[REACTION_MATRIX_COL2] = color3ToColor1; + this.uniformValues[REACTION_MATRIX_COL2 + 1] = color3ToColor2; + this.uniformValues[REACTION_MATRIX_COL2 + 2] = color3ToColor3; + this.uniformValues[SCALAR_BASE + 0] = moveSpeed * deltaTime; + this.uniformValues[SCALAR_BASE + 1] = turnSpeed * deltaTime; const sensorAngle = (sensorOffsetAngle * Math.PI) / 180; - this.uniformValues[2] = Math.sin(sensorAngle); - this.uniformValues[3] = Math.cos(sensorAngle); - this.uniformValues[4] = sensorOffsetDistance; - this.uniformValues[5] = turnWhenLost; - this.uniformValues[6] = individualTrailWeight; - this.uniformUintValues[7] = Math.max(0, Math.floor(agentCount)); - this.uniformValues[8] = introProgress ?? 1; - this.uniformValues[9] = color1ToColor1; - this.uniformValues[10] = color1ToColor2; - this.uniformValues[11] = color1ToColor3; - this.uniformValues[12] = color2ToColor1; - this.uniformValues[13] = color2ToColor2; - this.uniformValues[14] = color2ToColor3; - this.uniformValues[15] = color3ToColor1; - this.uniformValues[16] = color3ToColor2; - this.uniformValues[17] = color3ToColor3; - this.uniformValues[18] = forwardRotationScale; - this.uniformValues[19] = introNearDistanceInner; - this.uniformValues[20] = introNearDistanceMin; - this.uniformValues[21] = introNearSensorOffsetMultiplier; - this.uniformValues[22] = introTargetAngleBlend; - this.uniformValues[23] = introProgressCutoff; - this.uniformValues[24] = introTurnRateMultiplier; - this.uniformValues[25] = introRandomTurnMultiplier; - this.uniformValues[26] = introFarMoveMultiplier; - this.uniformValues[27] = introNearMoveMultiplier; - this.uniformValues[28] = introStepStopDistance; - this.uniformUintValues[29] = Math.max(0, Math.floor(time * randomTimeScale)) >>> 0; + this.uniformValues[SCALAR_BASE + 2] = Math.sin(sensorAngle); + this.uniformValues[SCALAR_BASE + 3] = Math.cos(sensorAngle); + this.uniformValues[SCALAR_BASE + 4] = sensorOffsetDistance; + this.uniformValues[SCALAR_BASE + 5] = turnWhenLost; + this.uniformValues[SCALAR_BASE + 6] = individualTrailWeight; + this.uniformUintValues[SCALAR_BASE + 7] = Math.max(0, Math.floor(agentCount)); + this.uniformValues[SCALAR_BASE + 8] = resolvedIntroProgress; + this.uniformValues[SCALAR_BASE + 9] = forwardRotationScale; + this.uniformValues[SCALAR_BASE + 10] = introNearDistanceInner; + this.uniformValues[SCALAR_BASE + 11] = introNearDistanceMin; + this.uniformValues[SCALAR_BASE + 12] = introNearSensorOffsetMultiplier; + this.uniformValues[SCALAR_BASE + 13] = introTargetAngleBlend; + this.uniformValues[SCALAR_BASE + 14] = introProgressCutoff; + this.uniformValues[SCALAR_BASE + 15] = introTurnRateMultiplier; + this.uniformValues[SCALAR_BASE + 16] = introRandomTurnMultiplier; + this.uniformValues[SCALAR_BASE + 17] = introMoveSpeed * deltaTime; + this.uniformValues[SCALAR_BASE + 18] = introStepStopDistance; + this.uniformUintValues[SCALAR_BASE + 19] = + Math.max(0, Math.floor(time * randomTimeScale)) >>> 0; writeBufferIfChanged( this.device, this.uniforms, @@ -219,7 +239,9 @@ export class AgentPipeline { const passEncoder = commandEncoder.beginComputePass( timestampWrites ? { timestampWrites } : undefined ); - passEncoder.setPipeline(this.pipeline); + passEncoder.setPipeline( + this.useSteadyPipeline ? this.pipelineSteady : this.pipelineFull + ); this.commonState.execute(passEncoder); passEncoder.setBindGroup( 1, diff --git a/src/pipelines/agents/agent.wgsl b/src/pipelines/agents/agent.wgsl index 43fe63f..591fa10 100644 --- a/src/pipelines/agents/agent.wgsl +++ b/src/pipelines/agents/agent.wgsl @@ -2,7 +2,16 @@ const PI: f32 = 3.14159265359; const TAU: f32 = 6.28318530718; const INV_TAU: f32 = 0.15915494309; +const CHANNEL_MASKS = array, 3>( + vec3(1.0, 0.0, 0.0), + vec3(0.0, 1.0, 0.0), + vec3(0.0, 0.0, 1.0), +); + struct Settings { + // Columns are indexed by source colorIndex; each column holds the per-target + // weights (colorXToColor1, colorXToColor2, colorXToColor3). + reactionMatrix: mat3x3, moveRate: f32, turnRate: f32, sensorAngleSin: f32, @@ -12,15 +21,6 @@ struct Settings { individualTrailWeight: f32, agentCount: u32, introProgress: f32, - color1ToColor1: f32, - color1ToColor2: f32, - color1ToColor3: f32, - color2ToColor1: f32, - color2ToColor2: f32, - color2ToColor3: f32, - color3ToColor1: f32, - color3ToColor2: f32, - color3ToColor3: f32, forwardRotationScale: f32, introNearDistanceInner: f32, introNearDistanceMin: f32, @@ -29,8 +29,7 @@ struct Settings { introProgressCutoff: f32, introTurnRateMultiplier: f32, introRandomTurnMultiplier: f32, - introFarMoveMultiplier: f32, - introNearMoveMultiplier: f32, + introMoveRate: f32, introStepStopDistance: f32, randomTimeSeed: u32, }; @@ -39,6 +38,11 @@ struct Settings { @group(1) @binding(2) var trailMapIn: texture_2d; @group(1) @binding(3) var trailMapOut: texture_storage_2d; +struct AgentMovement { + rotation: f32, + step: vec2, +} + @compute @workgroup_size(agentWorkgroupSize) fn main( @builtin(global_invocation_id) global_id: vec3 @@ -54,8 +58,8 @@ fn main( return; } - var position = agents[id].position; - var angle = agents[id].angle; + let position = agents[id].position; + let angle = agents[id].angle; var targetPosition = vec2(-1.0, -1.0); var hasIntroTarget = false; if settings.introProgress < settings.introProgressCutoff { @@ -70,92 +74,153 @@ fn main( let reactionMask = get_reaction_mask(colorIndex); let randomSeed = random_seed(id); let maxPosition = state.size - vec2(1.0, 1.0); - var rotation = 0.0; - var step = vec2(0.0, 0.0); + var movement = AgentMovement(0.0, vec2(0.0, 0.0)); if hasIntroTarget { - let introTargetOffset = targetPosition - position; - let introTargetDistance = length(introTargetOffset); - let targetAngle = atan2(introTargetOffset.y, introTargetOffset.x); - let nearTitle = 1.0 - smoothstep( - settings.introNearDistanceInner, - max( - settings.introNearDistanceMin, - settings.sensorOffset * settings.introNearSensorOffsetMultiplier - ), - introTargetDistance - ); - let desiredAngle = mix( - targetAngle, - agents[id].targetAngle, - nearTitle * settings.introTargetAngleBlend - ); - let introTurn = angle_delta(angle, desiredAngle); - - rotation = clamp( - introTurn, - -settings.turnRate * settings.introTurnRateMultiplier, - settings.turnRate * settings.introTurnRateMultiplier - ) - + (random_float(randomSeed + 1013904223u) - 0.5) * - settings.turnWhenLost * - settings.introRandomTurnMultiplier; - let moveRate = min( - settings.moveRate * - mix(settings.introFarMoveMultiplier, settings.introNearMoveMultiplier, nearTitle), - introTargetDistance - ); - if introTargetDistance > settings.introStepStopDistance { - step = introTargetOffset / introTargetDistance * moveRate; - } + movement = intro_decide(id, position, angle, targetPosition, randomSeed); } else { - let randomTurn = random_float(randomSeed); - let direction = vec2(cos(angle), sin(angle)); - - let forwardSensor = sensor_position(position, direction, settings.sensorOffset, maxPosition); - let leftSensor = sensor_position( - position, - rotate_direction(direction, settings.sensorAngleSin, settings.sensorAngleCos), - settings.sensorOffset, - maxPosition - ); - let rightSensor = sensor_position( - position, - rotate_direction(direction, -settings.sensorAngleSin, settings.sensorAngleCos), - settings.sensorOffset, - maxPosition - ); - - let trailForward = textureLoad(trailMapIn, forwardSensor, 0); - let trailLeft = textureLoad(trailMapIn, leftSensor, 0); - let trailRight = textureLoad(trailMapIn, rightSensor, 0); - - let weightForward = dot(trailForward.rgb, reactionMask); - let weightLeft = dot(trailLeft.rgb, reactionMask); - let weightRight = dot(trailRight.rgb, reactionMask); - - rotation = (randomTurn - 0.5) * settings.turnWhenLost; - if weightForward >= weightLeft && weightForward >= weightRight { - rotation = rotation * settings.forwardRotationScale; - } else { - rotation += sign(weightLeft - weightRight) * settings.turnRate; - } - - step = direction * settings.moveRate; + movement = steady_decide(position, angle, reactionMask, randomSeed, maxPosition); } - let nextPosition = clamp(position + step, vec2(0, 0), maxPosition); + agent_finalize(id, position, angle, channelMask, randomSeed, maxPosition, movement); +} + +// Steady-state-only entry point used after introProgress >= introProgressCutoff. +// Drops the intro target reads, atan2/smoothstep math, and introDelay check — +// once intro completes those paths are dead for the rest of the session. +@compute @workgroup_size(agentWorkgroupSize) +fn mainSteady( + @builtin(global_invocation_id) global_id: vec3 +) { + let id = get_id(global_id); + + if id >= settings.agentCount { + return; + } + + let colorIndex = agents[id].colorIndex; + if colorIndex < 0.0 || colorIndex >= 2.5 { + return; + } + + let position = agents[id].position; + let angle = agents[id].angle; + let channelMask = get_channel_mask(colorIndex); + let reactionMask = get_reaction_mask(colorIndex); + let randomSeed = random_seed(id); + let maxPosition = state.size - vec2(1.0, 1.0); + + let movement = steady_decide(position, angle, reactionMask, randomSeed, maxPosition); + agent_finalize(id, position, angle, channelMask, randomSeed, maxPosition, movement); +} + +fn steady_decide( + position: vec2, + angle: f32, + reactionMask: vec3, + randomSeed: u32, + maxPosition: vec2 +) -> AgentMovement { + let randomTurn = random_float(randomSeed); + let direction = vec2(cos(angle), sin(angle)); + + let forwardSensor = sensor_position(position, direction, settings.sensorOffset, maxPosition); + let leftSensor = sensor_position( + position, + rotate_direction(direction, settings.sensorAngleSin, settings.sensorAngleCos), + settings.sensorOffset, + maxPosition + ); + let rightSensor = sensor_position( + position, + rotate_direction(direction, -settings.sensorAngleSin, settings.sensorAngleCos), + settings.sensorOffset, + maxPosition + ); + + let trailForward = textureLoad(trailMapIn, forwardSensor, 0); + let trailLeft = textureLoad(trailMapIn, leftSensor, 0); + let trailRight = textureLoad(trailMapIn, rightSensor, 0); + + let weightForward = dot(trailForward.rgb, reactionMask); + let weightLeft = dot(trailLeft.rgb, reactionMask); + let weightRight = dot(trailRight.rgb, reactionMask); + + var rotation = (randomTurn - 0.5) * settings.turnWhenLost; + if weightForward >= weightLeft && weightForward >= weightRight { + rotation = rotation * settings.forwardRotationScale; + } else { + rotation += sign(weightLeft - weightRight) * settings.turnRate; + } + + return AgentMovement(rotation, direction * settings.moveRate); +} + +fn intro_decide( + id: u32, + position: vec2, + angle: f32, + targetPosition: vec2, + randomSeed: u32 +) -> AgentMovement { + let introTargetOffset = targetPosition - position; + let introTargetDistance = length(introTargetOffset); + let targetAngle = atan2(introTargetOffset.y, introTargetOffset.x); + let nearTitle = 1.0 - smoothstep( + settings.introNearDistanceInner, + max( + settings.introNearDistanceMin, + settings.sensorOffset * settings.introNearSensorOffsetMultiplier + ), + introTargetDistance + ); + let desiredAngle = mix( + targetAngle, + agents[id].targetAngle, + nearTitle * settings.introTargetAngleBlend + ); + let introTurn = angle_delta(angle, desiredAngle); + + let rotation = clamp( + introTurn, + -settings.turnRate * settings.introTurnRateMultiplier, + settings.turnRate * settings.introTurnRateMultiplier + ) + + (random_float(randomSeed + 1013904223u) - 0.5) * + settings.turnWhenLost * + settings.introRandomTurnMultiplier; + let moveRate = min(settings.introMoveRate, introTargetDistance); + var step = vec2(0.0, 0.0); + if introTargetDistance > settings.introStepStopDistance { + step = introTargetOffset / introTargetDistance * moveRate; + } + return AgentMovement(rotation, step); +} + +fn agent_finalize( + id: u32, + position: vec2, + angle: f32, + channelMask: vec3, + randomSeed: u32, + maxPosition: vec2, + movement: AgentMovement +) { + let nextPosition = clamp(position + movement.step, vec2(0, 0), maxPosition); + var rotation = movement.rotation; if nextPosition.x == 0 || nextPosition.x == maxPosition.x || nextPosition.y == 0 || nextPosition.y == maxPosition.y { rotation = PI + random_float(randomSeed + 22695477u) - 0.5; } - var trailBelow = textureLoad(trailMapIn, vec2(nextPosition), 0); - trailBelow = vec4( - trailBelow.rgb + channelMask * settings.individualTrailWeight, - max(trailBelow.a, 0.0) + // Writes only the deposit into a per-frame-cleared depositMap. The diffusion + // pass sums trailMap + depositMap at tile-load time, so the previous trail + // value is no longer needed here. Alpha stays 0 in depositMap — diffuse's + // alpha decay reads it from trailMap (where deposit alpha contributes 0). + textureStore( + trailMapOut, + vec2(nextPosition), + vec4(channelMask * settings.individualTrailWeight, 0.0) ); - - textureStore(trailMapOut, vec2(nextPosition), trailBelow); agents[id].angle = angle + rotation; agents[id].position = nextPosition; } @@ -181,41 +246,11 @@ fn rotate_direction(direction: vec2, angleSin: f32, angleCos: f32) -> vec2< } fn get_channel_mask(colorIndex: f32) -> vec3 { - if colorIndex < 0.5 { - return vec3(1, 0, 0); - } - if colorIndex < 1.5 { - return vec3(0, 1, 0); - } - if colorIndex < 2.5 { - return vec3(0, 0, 1); - } - return vec3(0.0, 0.0, 0.0); + return CHANNEL_MASKS[u32(clamp(colorIndex, 0.0, 2.0))]; } fn get_reaction_mask(colorIndex: f32) -> vec3 { - if colorIndex < 0.5 { - return vec3( - settings.color1ToColor1, - settings.color1ToColor2, - settings.color1ToColor3 - ); - } - if colorIndex < 1.5 { - return vec3( - settings.color2ToColor1, - settings.color2ToColor2, - settings.color2ToColor3 - ); - } - if colorIndex < 2.5 { - return vec3( - settings.color3ToColor1, - settings.color3ToColor2, - settings.color3ToColor3 - ); - } - return vec3(0.0, 0.0, 0.0); + return settings.reactionMatrix[u32(clamp(colorIndex, 0.0, 2.0))]; } fn angle_delta(sourceAngle: f32, targetAngle: f32) -> f32 { diff --git a/src/pipelines/brush/brush-pipeline.ts b/src/pipelines/brush/brush-pipeline.ts index 32584fa..7302f14 100644 --- a/src/pipelines/brush/brush-pipeline.ts +++ b/src/pipelines/brush/brush-pipeline.ts @@ -1,6 +1,7 @@ import { vec2 } from 'gl-matrix'; import { appConfig } from '../../config'; +import { getRenderQualityBrushSize } from '../../config/brush-size'; import { createCachedBufferWrite, writeBufferIfChanged, @@ -28,6 +29,7 @@ export interface BrushSettings { } interface BrushParameters extends BrushSettings { + internalRenderAreaMegapixels: number; pixelRatio?: number; selectedColorIndex: number; } @@ -50,12 +52,16 @@ const setBrushUniformValues = ( brushGrainNoiseOffsetY, brushGrainMinStrength, brushGrainMaxStrength, + internalRenderAreaMegapixels, selectedColorIndex, pixelRatio, }: BrushParameters ): void => { const safePixelRatio = getSafePixelRatio(pixelRatio); - const brushRadius = (brushSize * safePixelRatio) / 2; + const brushRadius = + (getRenderQualityBrushSize(brushSize, internalRenderAreaMegapixels) * + safePixelRatio) / + 2; target[0] = brushRadius; target[1] = brushRadius * brushRadius; diff --git a/src/pipelines/diffusion/diffuse.wgsl b/src/pipelines/diffusion/diffuse.wgsl index 684b37a..57bf139 100644 --- a/src/pipelines/diffusion/diffuse.wgsl +++ b/src/pipelines/diffusion/diffuse.wgsl @@ -27,6 +27,10 @@ const HASH_TO_UNIT_FLOAT: f32 = 2.3283064365386963e-10; @group(0) @binding(0) var settings: Settings; @group(0) @binding(1) var trailMap: texture_2d; @group(0) @binding(2) var trailMapOut: texture_storage_2d; +// Per-frame deposit accumulator written sparsely by agents. Summed with +// trailMap at tile-load so deposits propagate through the diffusion kernel +// in the same frame. +@group(0) @binding(3) var depositMap: texture_2d; var tile: array, 324>; var tileTrailStrength: array; @@ -49,7 +53,8 @@ fn main( vec2(0, 0), textureBound ); - let texel = textureLoad(trailMap, sourcePixel, 0); + let texel = textureLoad(trailMap, sourcePixel, 0) + + textureLoad(depositMap, sourcePixel, 0); tile[tileIndex] = texel; tileTrailStrength[tileIndex] = length(texel.rgb); } diff --git a/src/pipelines/diffusion/diffusion-pipeline.ts b/src/pipelines/diffusion/diffusion-pipeline.ts index eb92128..373e736 100644 --- a/src/pipelines/diffusion/diffusion-pipeline.ts +++ b/src/pipelines/diffusion/diffusion-pipeline.ts @@ -1,7 +1,7 @@ import { vec2 } from 'gl-matrix'; import { appConfig } from '../../config'; -import { createBindGroupCache } from '../../utils/graphics/bind-group-cache'; +import { createBindGroupCache3 } from '../../utils/graphics/bind-group-cache'; import { createCachedBufferWrite, writeBufferIfChanged, @@ -69,20 +69,29 @@ export class DiffusionPipeline { private readonly bindGroupLayout: GPUBindGroupLayout; private readonly pipeline: GPUComputePipeline; private readonly uniforms: GPUBuffer; + // 1x1 zero texture used as the depositMap binding when callers don't supply + // one (e.g. source-map diffusion). WebGPU's textureLoad returns zero for + // out-of-bounds coordinates, so the diffusion shader sums in zeros. + private readonly emptyDepositTexture: GPUTexture; + private readonly emptyDepositTextureView: GPUTextureView; private readonly uniformValues = new Float32Array(DiffusionPipeline.UNIFORM_COUNT); private readonly uniformCache = createCachedBufferWrite( DiffusionPipeline.UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT ); - private readonly getBindGroup = createBindGroupCache( - (trailMapIn, trailMapOut) => - this.device.createBindGroup({ - layout: this.bindGroupLayout, - entries: [ - { binding: 0, resource: { buffer: this.uniforms } }, - { binding: 1, resource: trailMapIn }, - { binding: 2, resource: trailMapOut }, - ], - }) + private readonly getBindGroup = createBindGroupCache3< + GPUTextureView, + GPUTextureView, + GPUTextureView + >((trailMapIn, trailMapOut, depositMap) => + this.device.createBindGroup({ + layout: this.bindGroupLayout, + entries: [ + { binding: 0, resource: { buffer: this.uniforms } }, + { binding: 1, resource: trailMapIn }, + { binding: 2, resource: trailMapOut }, + { binding: 3, resource: depositMap }, + ], + }) ); public constructor(private readonly device: GPUDevice) { @@ -104,6 +113,26 @@ export class DiffusionPipeline { size: DiffusionPipeline.UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, }); + + this.emptyDepositTexture = device.createTexture({ + format: TRAIL_SOURCE_TEXTURE_FORMAT, + size: { width: 1, height: 1 }, + usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT, + }); + this.emptyDepositTextureView = this.emptyDepositTexture.createView(); + const clearEncoder = device.createCommandEncoder(); + const clearPass = clearEncoder.beginRenderPass({ + colorAttachments: [ + { + view: this.emptyDepositTextureView, + clearValue: { r: 0, g: 0, b: 0, a: 0 }, + loadOp: 'clear', + storeOp: 'store', + }, + ], + }); + clearPass.end(); + device.queue.submit([clearEncoder.finish()]); } public setParameters({ @@ -135,9 +164,14 @@ export class DiffusionPipeline { trailMapIn: GPUTextureView, trailMapOut: GPUTextureView, size: vec2, + depositMap: GPUTextureView | null, timestampWrites?: GPUComputePassTimestampWrites ) { - const bindGroup = this.getBindGroup(trailMapIn, trailMapOut); + const bindGroup = this.getBindGroup( + trailMapIn, + trailMapOut, + depositMap ?? this.emptyDepositTextureView + ); const passEncoder = commandEncoder.beginComputePass( timestampWrites ? { timestampWrites } : undefined @@ -153,6 +187,7 @@ export class DiffusionPipeline { public destroy() { this.uniforms.destroy(); + this.emptyDepositTexture.destroy(); } private static get bindGroupLayout(): GPUBindGroupLayoutDescriptor { @@ -180,6 +215,13 @@ export class DiffusionPipeline { format: TRAIL_SOURCE_TEXTURE_FORMAT, }, }, + { + binding: 3, + visibility: GPUShaderStage.COMPUTE, + texture: { + sampleType: 'float', + }, + }, ], }; } diff --git a/src/pipelines/eraser/eraser-agent-pipeline.ts b/src/pipelines/eraser/eraser-agent-pipeline.ts index faef27c..4a85c14 100644 --- a/src/pipelines/eraser/eraser-agent-pipeline.ts +++ b/src/pipelines/eraser/eraser-agent-pipeline.ts @@ -86,7 +86,7 @@ export class EraserAgentPipeline { usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, }); - this.workgroupSize = getAgentWorkgroupSize(device); + this.workgroupSize = getAgentWorkgroupSize(device, 'eraser'); this.pipeline = device.createComputePipeline({ layout: device.createPipelineLayout({ bindGroupLayouts: [emptyBindGroupLayout, this.bindGroupLayout], @@ -94,7 +94,7 @@ export class EraserAgentPipeline { compute: { module: smartCompile( device, - substituteAgentWorkgroupSize(device, agentSchema), + substituteAgentWorkgroupSize(device, agentSchema, 'eraser'), shader ), entryPoint: 'main', diff --git a/src/settings.ts b/src/settings.ts index ee3d541..49e2518 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -43,6 +43,10 @@ export const settings: GardenRuntimeSettings = { ...buildSettings(activeVibe), }; +export const rememberActiveVibeSelection = (): void => { + writeBrowserStorage(appConfig.storage.vibeKey, activeVibe.id); +}; + export const applyVibeSettings = (vibe: VibePreset) => { activeVibe = cloneVibePreset(vibe); const nextSettings = buildSettings(activeVibe); @@ -59,7 +63,7 @@ export const applyVibeSettings = (vibe: VibePreset) => { normalizeRuntimeSettings(nextSettings, appConfig.runtimeSettings.controls) ); - writeBrowserStorage(appConfig.storage.vibeKey, vibe.id); + rememberActiveVibeSelection(); return activeVibe; }; diff --git a/src/style/_config-pane.scss b/src/style/_config-pane.scss index f677b4b..71ed186 100644 --- a/src/style/_config-pane.scss +++ b/src/style/_config-pane.scss @@ -41,6 +41,9 @@ touch-action: pan-y; -webkit-overflow-scrolling: touch; + // Tweakpane v4 internal classes — re-verify on upgrade. + // No public theming hook exists for label padding or the slider/number + // flex ratio; if a fourth override appears here, switch to a custom plugin. .tp-lblv_l { padding-right: 10px; } @@ -139,6 +142,7 @@ font-size: 11px; + // Tweakpane v4 internal class — re-verify on upgrade. .tp-sldtxtv_t { flex-basis: 48px; } diff --git a/src/style/toolbar/_layout.scss b/src/style/toolbar/_layout.scss index 6bae351..e11a841 100644 --- a/src/style/toolbar/_layout.scss +++ b/src/style/toolbar/_layout.scss @@ -5,16 +5,20 @@ --toolbar-background-strength: 0; --toolbar-divider-space: clamp(6px, 1.8vw, 14px); --toolbar-top-max-width: 594px; + --vibe-button-hit-size: 64px; display: grid; grid-template-areas: 'previous controls next' 'previous divider next' 'previous buttons next'; - grid-template-columns: auto minmax(0, 1fr) auto; + grid-template-columns: + var(--vibe-button-hit-size) + minmax(0, var(--toolbar-top-max-width)) + var(--vibe-button-hit-size); align-items: stretch; justify-content: center; - width: 100%; + width: fit-content; max-width: 100%; margin: 0 auto; padding-inline: clamp(8px, 1.4vw, 14px); @@ -85,9 +89,9 @@ position: relative; display: grid; place-items: center; - width: 52px; + width: var(--vibe-button-hit-size); height: auto; - min-height: 66px; + min-height: 72px; flex: 0 0 auto; padding: 0; border-radius: 0; diff --git a/src/style/toolbar/_responsive.scss b/src/style/toolbar/_responsive.scss index 5cd2963..658c40b 100644 --- a/src/style/toolbar/_responsive.scss +++ b/src/style/toolbar/_responsive.scss @@ -4,6 +4,7 @@ @include on-small-screen { --toolbar-divider-space: 4px; --toolbar-top-max-width: 329px; + --vibe-button-hit-size: 44px; grid-template-areas: 'previous controls next' @@ -15,7 +16,7 @@ row-gap: 0; > .vibe-button { - width: 36px; + width: var(--vibe-button-hit-size); min-height: 44px; &::before { diff --git a/src/vibe-uri.test.ts b/src/vibe-uri.test.ts new file mode 100644 index 0000000..6cc602a --- /dev/null +++ b/src/vibe-uri.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from 'vitest'; + +import { VibeId } from './config/types'; +import { createVibeUri, getVibeIdFromUri } from './vibe-uri'; + +describe('vibe URI handling', () => { + it('loads vibes from slug IDs and display names', () => { + expect(getVibeIdFromUri('https://example.test/?vibe=aurora-mycelium')).toBe( + VibeId.AuroraMycelium + ); + expect(getVibeIdFromUri('https://example.test/?vibe=Aurora%20Mycelium')).toBe( + VibeId.AuroraMycelium + ); + expect(getVibeIdFromUri('https://example.test/?vibe=velvet%20observatory')).toBe( + VibeId.VelvetObservatory + ); + }); + + it('uses query values before path or hash fallbacks', () => { + expect( + getVibeIdFromUri( + 'https://example.test/chrome-pollen?vibe=lichen-signal#vibe=aurora-mycelium' + ) + ).toBe(VibeId.LichenSignal); + }); + + it('accepts explicit path segments and hash fallbacks', () => { + expect(getVibeIdFromUri('https://example.test/vibes/tidepool-lantern')).toBe( + VibeId.TidepoolLantern + ); + expect(getVibeIdFromUri('https://example.test/#paper-lantern-fog')).toBe( + VibeId.PaperLanternFog + ); + }); + + it('ignores unknown or malformed vibe values', () => { + expect(getVibeIdFromUri('https://example.test/?vibe=missing')).toBeNull(); + expect(getVibeIdFromUri('https://example.test/?vibe=%E0%A4%A')).toBeNull(); + expect(getVibeIdFromUri('not a url')).toBeNull(); + }); + + it('creates a canonical query URI without dropping other URL parts', () => { + expect( + createVibeUri('https://example.test/garden?debug=1#panel', VibeId.ChromePollen) + ).toBe('/garden?debug=1&vibe=chrome-pollen#panel'); + + expect( + createVibeUri( + 'https://example.test/garden?vibe=aurora-mycelium&debug=1', + VibeId.LichenSignal + ) + ).toBe('/garden?vibe=lichen-signal&debug=1'); + }); +}); diff --git a/src/vibe-uri.ts b/src/vibe-uri.ts new file mode 100644 index 0000000..c8ee804 --- /dev/null +++ b/src/vibe-uri.ts @@ -0,0 +1,148 @@ +import type { VibeId } from './config/types'; +import { vibePresets } from './config/vibe-presets'; + +const VIBE_URI_QUERY_PARAM = 'vibe'; +const FALLBACK_URL_ORIGIN = 'https://fleeting.garden'; + +const slugifyVibeName = (value: string): string => + value + .normalize('NFKD') + .replace(/[\u0300-\u036f]/g, '') + .trim() + .toLowerCase() + .replace(/&/g, ' and ') + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); + +const safeDecodeURIComponent = (value: string): string => { + try { + return decodeURIComponent(value); + } catch { + return value; + } +}; + +const normalizeVibeIdentifier = (value: string): string => + slugifyVibeName(safeDecodeURIComponent(value).replace(/^[#/\\?\s]+|[/\\?\s]+$/g, '')); + +const vibeIdByIdentifier = new Map(); + +for (const vibe of vibePresets) { + vibeIdByIdentifier.set(normalizeVibeIdentifier(vibe.id), vibe.id); + vibeIdByIdentifier.set(normalizeVibeIdentifier(vibe.name), vibe.id); +} + +const toUrl = (url: string | URL): URL | null => { + try { + return new URL(url, FALLBACK_URL_ORIGIN); + } catch { + return null; + } +}; + +const resolveVibeId = (value: string | null | undefined): VibeId | null => { + if (!value) { + return null; + } + + return vibeIdByIdentifier.get(normalizeVibeIdentifier(value)) ?? null; +}; + +const getHashSearchParam = (hash: string): string | null => { + const hashValue = hash.replace(/^#/, ''); + if (!hashValue.includes('=')) { + return null; + } + + const searchText = hashValue.startsWith('?') ? hashValue.slice(1) : hashValue; + try { + return new URLSearchParams(searchText).get(VIBE_URI_QUERY_PARAM); + } catch { + return null; + } +}; + +const getPathVibeCandidates = (pathname: string): Array => { + const segments = pathname.split('/').map(safeDecodeURIComponent).filter(Boolean); + const explicitVibeIndex = segments.findIndex((segment) => + ['vibe', 'vibes'].includes(segment.toLowerCase()) + ); + + return [ + explicitVibeIndex >= 0 ? segments[explicitVibeIndex + 1] : undefined, + segments.at(-1), + ].filter((candidate): candidate is string => typeof candidate === 'string'); +}; + +export const getVibeIdFromUri = (url: string | URL): VibeId | null => { + const parsedUrl = toUrl(url); + if (!parsedUrl) { + return null; + } + + const candidates = [ + parsedUrl.searchParams.get(VIBE_URI_QUERY_PARAM), + getHashSearchParam(parsedUrl.hash), + ...getPathVibeCandidates(parsedUrl.pathname), + parsedUrl.hash.replace(/^#/, ''), + ]; + + for (const candidate of candidates) { + const vibeId = resolveVibeId(candidate); + if (vibeId) { + return vibeId; + } + } + + return null; +}; + +export const getCurrentUriVibeId = (): VibeId | null => { + if (typeof window === 'undefined') { + return null; + } + + return getVibeIdFromUri(window.location.href); +}; + +const getVibeSlug = (vibeId: VibeId): string => { + const vibe = vibePresets.find((preset) => preset.id === vibeId); + return vibe ? slugifyVibeName(vibe.name) : vibeId; +}; + +export const createVibeUri = (url: string | URL, vibeId: VibeId): string => { + const parsedUrl = toUrl(url); + if (!parsedUrl) { + return `?${VIBE_URI_QUERY_PARAM}=${encodeURIComponent(getVibeSlug(vibeId))}`; + } + + parsedUrl.searchParams.set(VIBE_URI_QUERY_PARAM, getVibeSlug(vibeId)); + return `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`; +}; + +export const writeCurrentVibeUri = ( + vibeId: VibeId, + mode: 'push' | 'replace' = 'replace' +): void => { + if (typeof window === 'undefined') { + return; + } + + const nextUri = createVibeUri(window.location.href, vibeId); + const currentUri = `${window.location.pathname}${window.location.search}${window.location.hash}`; + if (nextUri === currentUri) { + return; + } + + const nextState = + typeof window.history.state === 'object' && window.history.state !== null + ? { ...window.history.state, vibeId } + : { vibeId }; + + if (mode === 'push') { + window.history.pushState(nextState, '', nextUri); + return; + } + + window.history.replaceState(nextState, '', nextUri); +}; diff --git a/src/vibes.ts b/src/vibes.ts index b6f1576..def4584 100644 --- a/src/vibes.ts +++ b/src/vibes.ts @@ -1,6 +1,7 @@ import { appConfig } from './config'; import { VibeId, type VibePreset } from './config/types'; import { readBrowserStorage } from './utils/browser-storage'; +import { getCurrentUriVibeId, getVibeIdFromUri } from './vibe-uri'; export { VibeId }; export type { VibePreset }; @@ -11,11 +12,17 @@ const VIBE_IDS = new Set(VIBE_PRESETS.map((vibe) => vibe.id)); const isVibeId = (value: unknown): value is VibeId => typeof value === 'string' && VIBE_IDS.has(value as VibeId); -export const getInitialVibe = (): VibePreset => { - const storedVibeId = readBrowserStorage(appConfig.storage.vibeKey); - const initialVibeId = isVibeId(storedVibeId) - ? storedVibeId - : appConfig.vibes.defaultVibeId; +export const getVibeById = (vibeId: VibeId): VibePreset | undefined => + VIBE_PRESETS.find((vibe) => vibe.id === vibeId); - return VIBE_PRESETS.find((vibe) => vibe.id === initialVibeId) ?? VIBE_PRESETS[0]; +export const getInitialVibe = (): VibePreset => { + const uriVibeId = getCurrentUriVibeId(); + const storedVibeId = readBrowserStorage(appConfig.storage.vibeKey); + const storedOrLegacyVibeId = isVibeId(storedVibeId) + ? storedVibeId + : getVibeIdFromUri(`?vibe=${encodeURIComponent(storedVibeId ?? '')}`); + const initialVibeId = + uriVibeId ?? storedOrLegacyVibeId ?? appConfig.vibes.defaultVibeId; + + return getVibeById(initialVibeId) ?? VIBE_PRESETS[0]; }; diff --git a/vite.config.ts b/vite.config.ts index 1818b2a..da6207c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -21,7 +21,6 @@ export default defineConfig(({ command }) => ({ cssMinify: 'lightningcss', }, server: { - open: true, host: true, hmr: false, }, From 646564fc734c6da59d43b09be2cec609e5b4b4cd Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Fri, 22 May 2026 08:03:13 +0100 Subject: [PATCH 2/5] Fixes --- src/config/vibe-presets.test.ts | 7 +-- src/config/vibe-presets.ts | 44 +++++++------- src/pipelines/render/render.wgsl | 100 +++++++++++++++++++++++++------ src/vibe-uri.test.ts | 2 +- 4 files changed, 108 insertions(+), 45 deletions(-) diff --git a/src/config/vibe-presets.test.ts b/src/config/vibe-presets.test.ts index 4052166..54c7c9e 100644 --- a/src/config/vibe-presets.test.ts +++ b/src/config/vibe-presets.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest'; import { vibePresets } from './vibe-presets'; const FINAL_VIBE_NAMES = [ - 'Aurora Mycelium', + 'Aurora Mycelium Copy', 'Velvet Observatory', 'Lichen Signal', 'Tidepool Lantern', @@ -31,10 +31,7 @@ describe('vibePresets', () => { ) .map((preset) => preset.name); - expect(blendedNames).toEqual([ - 'Aurora Mycelium', - 'Tidepool Lantern', - ]); + expect(blendedNames).toEqual(['Tidepool Lantern']); expect(softParticleNames).toEqual(['Chrome Pollen']); }); diff --git a/src/config/vibe-presets.ts b/src/config/vibe-presets.ts index e25911a..3b9a2b5 100644 --- a/src/config/vibe-presets.ts +++ b/src/config/vibe-presets.ts @@ -20,13 +20,13 @@ type ColorReactionSettings = Pick< const colorReactions = { auroraMycelium: { color1ToColor1: 1, - color1ToColor2: 1, + color1ToColor2: 0, color1ToColor3: 0, - color2ToColor1: 0, + color2ToColor1: -1, color2ToColor2: 1, - color2ToColor3: 1, - color3ToColor1: 1, - color3ToColor2: 0, + color2ToColor3: 0, + color3ToColor1: -1, + color3ToColor2: -1, color3ToColor3: 1, }, velvetObservatory: { @@ -137,38 +137,38 @@ export const defaultVibeId = VibeId.AuroraMycelium; export const vibePresets: Array = [ { id: VibeId.AuroraMycelium, - name: 'Aurora Mycelium', + name: 'Aurora Mycelium Copy', colors: [ - [78, 255, 176], + [221, 255, 78], [154, 99, 255], - [169, 238, 255], + [255, 31, 199], ], backgroundColor: [6, 13, 22], settings: { ...colorReactions.auroraMycelium, - backgroundGrainStrength: 0.014, - brushSize: 21, - clarity: 0.52, - decayRateTrails: 988, - forwardRotationScale: 0.28, - individualTrailWeight: 0.082, - moveSpeed: 54, + backgroundGrainStrength: 0.003, + brushSize: 8.75, + clarity: 0.379, + decayRateTrails: 940, + forwardRotationScale: 0, + individualTrailWeight: 0.121, + moveSpeed: 270, sensorOffsetAngle: 36, - sensorOffsetDistance: 76, - spawnPerPixel: 0.14, - strokeAngleJitterRadians: 1.45, - turnSpeed: 34, - turnWhenLost: 0.75, + sensorOffsetDistance: 51, + spawnPerPixel: 0.13999999999999999, + strokeAngleJitterRadians: 0.44999999999999996, + turnSpeed: 22, + turnWhenLost: 6.071532165918825e-17, }, audio: { ...defaultGardenAudioVibeSettings, idleIntensity: 0.12, bpm: 60, - rampUpIntensity: 0.7, + rampUpIntensity: 0.7000000000000001, rampUpTime: 0.14, noteLength: 0.86, notePitchOffset: -2, - brightness: 0.84, + brightness: 0.8400000000000001, scale: musicScales.lydian, progression: musicProgressions.aurora, }, diff --git a/src/pipelines/render/render.wgsl b/src/pipelines/render/render.wgsl index 65e9229..d5613bd 100644 --- a/src/pipelines/render/render.wgsl +++ b/src/pipelines/render/render.wgsl @@ -12,6 +12,14 @@ struct Settings { brushColorStrengthMultiplier: f32, }; +const COMMON_CHANNEL_REDUCTION: f32 = 0.75; +const OVERLAP_SATURATION_BOOST: f32 = 1.35; +const LOW_SATURATION_RESCUE_AMOUNT: f32 = 0.65; +const LOW_SATURATION_RESCUE_MIN: f32 = 0.08; +const LOW_SATURATION_RESCUE_MAX: f32 = 0.22; +const COLOR_WEIGHT_EPSILON: f32 = 0.0001; +const LUMA_WEIGHTS: vec3 = vec3(0.2126, 0.7152, 0.0722); + @group(1) @binding(0) var settings: Settings; @group(1) @binding(2) var trailMap: texture_2d; @group(1) @binding(3) var sourceMap: texture_2d; @@ -41,25 +49,13 @@ fn renderColor(traces: vec4, sources: vec4, background: vec3) -> } if brushStrength <= 0.0 { - let traceColor = - traceStrengths.r * settings.colorA - + traceStrengths.g * settings.colorB - + traceStrengths.b * settings.colorC; - let normalizedTraceColor = normalizeColorIntensity(traceColor); - return vec4(mix(background, clamp(normalizedTraceColor, vec3(0), vec3(1)), traceStrength), 1); + let traceColor = colorFromChannelStrengths(traceStrengths); + return vec4(mix(background, clamp(traceColor, vec3(0), vec3(1)), traceStrength), 1); } let strengths = max(traceStrengths, sourceStrengths); - let traceColor = - strengths.r * settings.colorA - + strengths.g * settings.colorB - + strengths.b * settings.colorC; - let normalizedTraceColor = normalizeColorIntensity(traceColor); - let brushColor = - sourceStrengths.r * settings.colorA - + sourceStrengths.g * settings.colorB - + sourceStrengths.b * settings.colorC; - let normalizedBrushColor = normalizeColorIntensity(brushColor); + let traceColor = colorFromChannelStrengths(strengths); + let brushColor = colorFromChannelStrengths(sourceStrengths); let brushVisibility = clamp( brushStrength * ( settings.brushColorBase + @@ -68,7 +64,7 @@ fn renderColor(traces: vec4, sources: vec4, background: vec3) -> 0, 1 ); - let color = max(normalizedTraceColor, normalizedBrushColor); + let color = mix(traceColor, brushColor, brushVisibility); let strength = max(maxComponent(strengths), brushVisibility); return vec4(mix(background, clamp(color, vec3(0), vec3(1)), strength), 1); @@ -78,10 +74,80 @@ fn maxComponent(v: vec3) -> f32 { return max(max(v.r, v.g), v.b); } +fn minComponent(v: vec3) -> f32 { + return min(min(v.r, v.g), v.b); +} + +fn componentSum(v: vec3) -> f32 { + return v.r + v.g + v.b; +} + fn clarity(strength: vec3) -> vec3 { return pow(clamp(strength, vec3(0), vec3(1)), vec3(settings.clarity)); } +fn colorFromChannelStrengths(strengths: vec3) -> vec3 { + if maxComponent(strengths) <= 0.0 { + return vec3(0.0); + } + + let weights = colorWeights(strengths); + let color = + weights.r * settings.colorA + + weights.g * settings.colorB + + weights.b * settings.colorC; + return preserveOverlapVibrancy(normalizeColorIntensity(color), strengths); +} + +fn colorWeights(strengths: vec3) -> vec3 { + let commonStrength = minComponent(strengths); + var weightBase = max( + strengths - vec3(commonStrength * COMMON_CHANNEL_REDUCTION), + vec3(0.0) + ); + if componentSum(weightBase) <= COLOR_WEIGHT_EPSILON { + weightBase = strengths; + } + + let sharpenedWeights = weightBase * weightBase; + return sharpenedWeights / max(COLOR_WEIGHT_EPSILON, componentSum(sharpenedWeights)); +} + +fn preserveOverlapVibrancy(color: vec3, strengths: vec3) -> vec3 { + let strongest = maxComponent(strengths); + let overlapAmount = clamp( + (componentSum(strengths) - strongest) / max(COLOR_WEIGHT_EPSILON, strongest), + 0.0, + 1.0 + ); + + let luminance = dot(color, LUMA_WEIGHTS); + var vibrantColor = clamp( + vec3(luminance) + + (color - vec3(luminance)) * + mix(1.0, OVERLAP_SATURATION_BOOST, overlapAmount), + vec3(0.0), + vec3(1.0) + ); + + let saturation = maxComponent(vibrantColor) - minComponent(vibrantColor); + let rescueAmount = + overlapAmount * + (1.0 - smoothstep(LOW_SATURATION_RESCUE_MIN, LOW_SATURATION_RESCUE_MAX, saturation)) * + LOW_SATURATION_RESCUE_AMOUNT; + return mix(vibrantColor, dominantColor(strengths), rescueAmount); +} + +fn dominantColor(strengths: vec3) -> vec3 { + if strengths.r >= strengths.g && strengths.r >= strengths.b { + return normalizeColorIntensity(settings.colorA); + } + if strengths.g >= strengths.b { + return normalizeColorIntensity(settings.colorB); + } + return normalizeColorIntensity(settings.colorC); +} + fn normalizeColorIntensity(color: vec3) -> vec3 { let brightestChannel = maxComponent(color); return color / max(settings.traceNormalizationFloor, brightestChannel); diff --git a/src/vibe-uri.test.ts b/src/vibe-uri.test.ts index 6cc602a..79f0109 100644 --- a/src/vibe-uri.test.ts +++ b/src/vibe-uri.test.ts @@ -8,7 +8,7 @@ describe('vibe URI handling', () => { expect(getVibeIdFromUri('https://example.test/?vibe=aurora-mycelium')).toBe( VibeId.AuroraMycelium ); - expect(getVibeIdFromUri('https://example.test/?vibe=Aurora%20Mycelium')).toBe( + expect(getVibeIdFromUri('https://example.test/?vibe=Aurora%20Mycelium%20Copy')).toBe( VibeId.AuroraMycelium ); expect(getVibeIdFromUri('https://example.test/?vibe=velvet%20observatory')).toBe( From a7c04b2bd818a092d11cdc42c2cfe39eed2fdce4 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Fri, 22 May 2026 08:08:31 +0100 Subject: [PATCH 3/5] fix zoom in --- src/style/common.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/style/common.scss b/src/style/common.scss index c579827..23c82aa 100644 --- a/src/style/common.scss +++ b/src/style/common.scss @@ -16,6 +16,7 @@ html { height: 100%; + touch-action: manipulation; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-rendering: optimizeLegibility; @@ -23,6 +24,7 @@ html { body { font-family: 'Open Sans', sans-serif; + touch-action: manipulation; } .visually-hidden { From 05c8a39bd84940cdf86df58464ca2e15837842ed Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 24 May 2026 09:34:46 +0100 Subject: [PATCH 4/5] Small improvements --- .gitignore | 2 ++ .prettierrc | 2 +- e2e/app.spec.ts | 10 ++++---- package.json | 2 +- src/audio/garden-audio-config.ts | 3 ++- src/audio/garden-audio-graph.ts | 6 ++--- src/audio/generative-piano.ts | 23 +++++++++-------- src/audio/noise-burst-player.ts | 4 +-- src/audio/piano-sampler.ts | 17 +++++++------ src/config.ts | 9 ++++--- src/config/vibe-presets.test.ts | 37 +++++++++++++++++++-------- src/config/vibe-presets.ts | 38 ++++++++++++++-------------- src/consts.ts | 10 -------- src/game-loop/frame-performance.ts | 26 +++++++++---------- src/game-loop/simulation-textures.ts | 3 +-- src/page/audio-control.ts | 8 +++--- src/page/splash-screen.ts | 11 ++++++++ src/style/vars.scss | 2 +- vite.config.ts | 1 - 19 files changed, 119 insertions(+), 95 deletions(-) delete mode 100644 src/consts.ts diff --git a/.gitignore b/.gitignore index 0f59a68..db39815 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ node_modules dist test-results +.DS_Store +*.log diff --git a/.prettierrc b/.prettierrc index 2a3ea53..743851d 100644 --- a/.prettierrc +++ b/.prettierrc @@ -6,5 +6,5 @@ "endOfLine": "lf", "plugins": ["@ianvs/prettier-plugin-sort-imports"], "importOrder": ["", "", "", "^[./]"], - "importOrderTypeScriptVersion": "5.0.0" + "importOrderTypeScriptVersion": "5.6.0" } diff --git a/e2e/app.spec.ts b/e2e/app.spec.ts index 6f50cb8..3285b30 100644 --- a/e2e/app.spec.ts +++ b/e2e/app.spec.ts @@ -71,7 +71,7 @@ test('starts the WebGPU garden and accepts drawing input', async ({ page }) => { const startButton = page.getByRole('button', { name: 'Start' }); await expect(startButton).toBeVisible(); await expect(startButton).toBeEnabled({ timeout: 30_000 }); - await startButton.click(); + await page.keyboard.press('Enter'); await expect(page.locator('body')).not.toHaveClass(/is-loading/, { timeout: 30_000, }); @@ -146,7 +146,7 @@ test('syncs the selected vibe with the URI', async ({ page }) => { await expect(page).toHaveURL(/vibe=bone-archive/); - await page.locator('.next-vibe').click(); + await page.getByRole('button', { name: 'Next vibe' }).click(); await expect(page).toHaveURL(/vibe=pelagic-caustics/); await page.goBack(); @@ -160,8 +160,8 @@ test('keeps audio focus outlines scoped to the active control', async ({ page }) await expect(page.locator('body')).not.toHaveClass(/is-loading/); const audioControl = page.locator('.audio-control'); - const soundButton = page.locator('button.sound'); - const volumeSlider = page.locator('.volume-slider'); + const soundButton = page.getByRole('button', { name: /audio/i }); + const volumeSlider = page.getByRole('slider', { name: 'Master volume' }); await soundButton.click(); await expect(audioControl).toHaveCSS('outline-style', 'none'); @@ -201,7 +201,7 @@ test('keeps the config overlay scrollable and dismissible on mobile', async ({ timeout: 30_000, }); - const settingsButton = page.locator('button.settings'); + const settingsButton = page.getByRole('button', { name: 'Show config overlay' }); await settingsButton.click(); const pane = page.locator('.config-pane'); diff --git a/package.json b/package.json index 9d07a0f..fdd05c4 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "generate-icons": "pwa-assets-generator" }, "engines": { - "node": ">=20" + "node": ">=22" }, "repository": { "type": "git", diff --git a/src/audio/garden-audio-config.ts b/src/audio/garden-audio-config.ts index bc6b599..da68c93 100644 --- a/src/audio/garden-audio-config.ts +++ b/src/audio/garden-audio-config.ts @@ -1,6 +1,7 @@ -import { DEFAULT_AUDIO_VOLUME } from '../consts'; import type { PianoNoteRole } from './garden-audio-types'; +const DEFAULT_AUDIO_VOLUME = 0.5; + type GardenAudioChordQuality = 'major' | 'minor' | 'sus2' | 'sus4'; export interface GardenAudioChord { diff --git a/src/audio/garden-audio-graph.ts b/src/audio/garden-audio-graph.ts index d992b95..de934f9 100644 --- a/src/audio/garden-audio-graph.ts +++ b/src/audio/garden-audio-graph.ts @@ -23,8 +23,8 @@ const graphTuning = { eventBusGain: 1, noiseMax: 1, noiseMin: -1, - latencyHint: 'interactive' as AudioContextLatencyCategory, - outputFilterType: 'highpass' as BiquadFilterType, + latencyHint: 'interactive', + outputFilterType: 'highpass', compressor: { thresholdDb: -18, kneeDb: 18, @@ -32,7 +32,7 @@ const graphTuning = { attackSeconds: 0.018, releaseSeconds: 0.18, }, -}; +} as const; const delayFilterTuning = { feedbackHighPassHz: 180, feedbackLowPassHz: 5200, diff --git a/src/audio/generative-piano.ts b/src/audio/generative-piano.ts index 5743964..fbe1201 100644 --- a/src/audio/generative-piano.ts +++ b/src/audio/generative-piano.ts @@ -17,6 +17,8 @@ import { PIANO_SCHEDULE_AHEAD_SECONDS } from './piano-sampler'; const GENERATIVE_LOOKAHEAD_SECONDS = 0.3; const GENERATIVE_START_DELAY_SECONDS = 0.02; +const TEXTURE_ONSET_EXPRESSION = 0.15; +const SUPPORT_ONSET_EXPRESSION = 0.4; const chordVoicings: Record< GardenAudioChord['quality'], @@ -1087,6 +1089,9 @@ export class GenerativePianoEngine { } private shouldPlaySupport(expression: number, barIndex: number): boolean { + if (expression < SUPPORT_ONSET_EXPRESSION) { + return false; + } if (expression >= generativePianoTuning.supportNote.expressionThreshold) { return true; } @@ -1098,19 +1103,17 @@ export class GenerativePianoEngine { } private shouldPlayTexture(expression: number, barIndex: number): boolean { + if (expression < TEXTURE_ONSET_EXPRESSION) { + return false; + } + if (expression >= generativePianoTuning.textureNote.mediumExpressionThreshold) { + return barIndex % generativePianoTuning.textureNote.intenseSpacing === 0; + } const spacing = expression < generativePianoTuning.textureNote.idleExpressionThreshold ? generativePianoTuning.idleTextureBarSpacing - : expression < generativePianoTuning.textureNote.mediumExpressionThreshold - ? generativePianoTuning.mediumTextureBarSpacing - : generativePianoTuning.textureNote.intenseSpacing; - - return ( - barIndex % spacing === - (spacing === generativePianoTuning.textureNote.intenseSpacing - ? 0 - : generativePianoTuning.textureNote.idlePhase) - ); + : generativePianoTuning.mediumTextureBarSpacing; + return barIndex % spacing === generativePianoTuning.textureNote.idlePhase % spacing; } private getSupportOffsets( diff --git a/src/audio/noise-burst-player.ts b/src/audio/noise-burst-player.ts index 19cc45b..7d148ac 100644 --- a/src/audio/noise-burst-player.ts +++ b/src/audio/noise-burst-player.ts @@ -8,8 +8,8 @@ const noiseBurstTuning = { offsetRandomSeconds: 0.4, scheduleAheadSeconds: 0.002, silentGain: 0.0001, - filterType: 'bandpass' as BiquadFilterType, -}; + filterType: 'bandpass', +} as const; export class NoiseBurstPlayer { public constructor(private readonly graph: GardenAudioGraph) {} diff --git a/src/audio/piano-sampler.ts b/src/audio/piano-sampler.ts index 1960b3b..a3f80f7 100644 --- a/src/audio/piano-sampler.ts +++ b/src/audio/piano-sampler.ts @@ -14,7 +14,7 @@ interface ActivePianoVoice { } const pianoSamplerTuning = { - filterType: 'lowpass' as BiquadFilterType, + filterType: 'lowpass', filterQ: 0.7, minDurationSeconds: 0.08, minFadeSeconds: 0.08, @@ -22,7 +22,7 @@ const pianoSamplerTuning = { tailStopExtraSeconds: 0.05, voiceStealFadeSeconds: 0.025, voiceStealStopSeconds: 0.05, -}; +} as const; export class PianoSampler { private samples: Array = []; @@ -178,7 +178,11 @@ export class PianoSampler { this.trimActiveVoices(scheduledStart); while (this.activeVoices.length >= this.config.piano.maxVoices) { - this.stopVoice(this.activeVoices.shift() as ActivePianoVoice, scheduledStart); + const oldest = this.activeVoices.shift(); + if (!oldest) { + break; + } + this.stopVoice(oldest, scheduledStart); } filter.type = pianoSamplerTuning.filterType; @@ -220,11 +224,8 @@ export class PianoSampler { ); } - private computeNoteGain(velocity: number, scale = 1): number { - return Math.max( - pianoSamplerTuning.minGain, - this.config.piano.gain * velocity * scale - ); + private computeNoteGain(velocity: number): number { + return Math.max(pianoSamplerTuning.minGain, this.config.piano.gain * velocity); } private findNearestSample(midi: number): LoadedPianoSample | null { diff --git a/src/config.ts b/src/config.ts index 473dbf8..27e8a24 100644 --- a/src/config.ts +++ b/src/config.ts @@ -3,7 +3,8 @@ import { defaultSettings } from './config/default-settings'; import { runtimeControls } from './config/runtime-controls'; import type { GardenAppConfig } from './config/types'; import { defaultVibeId, vibePresets } from './config/vibe-presets'; -import { APP_STORAGE_KEYS, DEFAULT_AUDIO_VOLUME } from './consts'; + +const DEFAULT_AUDIO_VOLUME = 0.5; export { normalizeNumberControlValue, @@ -114,9 +115,9 @@ export const appConfig = { }, }, storage: { - audioMutedKey: APP_STORAGE_KEYS.audioMuted, - audioVolumeKey: APP_STORAGE_KEYS.audioVolume, - vibeKey: APP_STORAGE_KEYS.vibe, + audioMutedKey: 'fleeting-garden:audio-muted', + audioVolumeKey: 'fleeting-garden:audio-volume', + vibeKey: 'fleeting-garden:vibe', }, toolbar: { eraser: { diff --git a/src/config/vibe-presets.test.ts b/src/config/vibe-presets.test.ts index 54c7c9e..608a86e 100644 --- a/src/config/vibe-presets.test.ts +++ b/src/config/vibe-presets.test.ts @@ -11,6 +11,19 @@ const FINAL_VIBE_NAMES = [ 'Chrome Pollen', ]; +const BLENDED_BRUSH_SIZE_MIN = 17; +const BLENDED_CLARITY_MAX = 0.56; +const SOFT_PARTICLE_BRUSH_SIZE_MAX = 5; +const SOFT_PARTICLE_CLARITY_MAX = 0.2; + +// Performance guardrails — bumping any of these is an explicit perf trade-off. +const MAX_SPAWN_PER_PIXEL = 0.38; +const MAX_BRUSH_SIZE = 36; +const HIGH_DENSITY_SPAWN_THRESHOLD = 0.28; +const HIGH_DENSITY_DECAY_LIMIT = 940; +const HIGH_DENSITY_BRUSH_SIZE_LIMIT = 14; +const HIGH_DENSITY_TRAIL_WEIGHT_LIMIT = 0.055; + describe('vibePresets', () => { it('keeps the classic preset set distinct', () => { expect(vibePresets.map((preset) => preset.name)).toEqual(FINAL_VIBE_NAMES); @@ -22,12 +35,16 @@ describe('vibePresets', () => { it('includes both blended and visibly particulate styles', () => { const blendedNames = vibePresets .filter( - (preset) => preset.settings.brushSize >= 17 && preset.settings.clarity <= 0.56 + (preset) => + preset.settings.brushSize >= BLENDED_BRUSH_SIZE_MIN && + preset.settings.clarity <= BLENDED_CLARITY_MAX ) .map((preset) => preset.name); const softParticleNames = vibePresets .filter( - (preset) => preset.settings.brushSize <= 5 && preset.settings.clarity <= 0.2 + (preset) => + preset.settings.brushSize <= SOFT_PARTICLE_BRUSH_SIZE_MAX && + preset.settings.clarity <= SOFT_PARTICLE_CLARITY_MAX ) .map((preset) => preset.name); @@ -40,17 +57,17 @@ describe('vibePresets', () => { const { name, settings } = preset; const presetViolations: Array = []; - if (settings.spawnPerPixel > 0.38) { - presetViolations.push(`${name} density exceeds 0.38`); + if (settings.spawnPerPixel > MAX_SPAWN_PER_PIXEL) { + presetViolations.push(`${name} density exceeds ${MAX_SPAWN_PER_PIXEL}`); } - if (settings.brushSize > 36) { - presetViolations.push(`${name} brush size exceeds 36`); + if (settings.brushSize > MAX_BRUSH_SIZE) { + presetViolations.push(`${name} brush size exceeds ${MAX_BRUSH_SIZE}`); } if ( - settings.spawnPerPixel >= 0.28 && - (settings.decayRateTrails > 940 || - settings.brushSize > 14 || - settings.individualTrailWeight > 0.055) + settings.spawnPerPixel >= HIGH_DENSITY_SPAWN_THRESHOLD && + (settings.decayRateTrails > HIGH_DENSITY_DECAY_LIMIT || + settings.brushSize > HIGH_DENSITY_BRUSH_SIZE_LIMIT || + settings.individualTrailWeight > HIGH_DENSITY_TRAIL_WEIGHT_LIMIT) ) { presetViolations.push(`${name} combines high density with too much persistence`); } diff --git a/src/config/vibe-presets.ts b/src/config/vibe-presets.ts index 3b9a2b5..c5c7d40 100644 --- a/src/config/vibe-presets.ts +++ b/src/config/vibe-presets.ts @@ -155,20 +155,20 @@ export const vibePresets: Array = [ moveSpeed: 270, sensorOffsetAngle: 36, sensorOffsetDistance: 51, - spawnPerPixel: 0.13999999999999999, - strokeAngleJitterRadians: 0.44999999999999996, + spawnPerPixel: 0.14, + strokeAngleJitterRadians: 0.45, turnSpeed: 22, - turnWhenLost: 6.071532165918825e-17, + turnWhenLost: 0, }, audio: { ...defaultGardenAudioVibeSettings, idleIntensity: 0.12, bpm: 60, - rampUpIntensity: 0.7000000000000001, + rampUpIntensity: 0.7, rampUpTime: 0.14, noteLength: 0.86, notePitchOffset: -2, - brightness: 0.8400000000000001, + brightness: 0.84, scale: musicScales.lydian, progression: musicProgressions.aurora, }, @@ -188,23 +188,23 @@ export const vibePresets: Array = [ brushSize: 9.75, clarity: 0.437, decayRateTrails: 915, - forwardRotationScale: 2.0816681711721685e-17, + forwardRotationScale: 0, individualTrailWeight: 0.1, moveSpeed: 216, sensorOffsetAngle: 24, sensorOffsetDistance: 17, spawnPerPixel: 0.24, - strokeAngleJitterRadians: 0.16999999999999993, + strokeAngleJitterRadians: 0.17, turnSpeed: 33, - turnWhenLost: 0.42000000000000004, + turnWhenLost: 0.42, }, audio: { ...defaultGardenAudioVibeSettings, idleIntensity: 0.55, bpm: 72, rampUpIntensity: 1.42, - rampUpTime: 0.07000000000000002, - noteLength: 0.7000000000000001, + rampUpTime: 0.07, + noteLength: 0.7, notePitchOffset: 0, brightness: 0.94, scale: musicScales.naturalMinor, @@ -227,21 +227,21 @@ export const vibePresets: Array = [ clarity: 0.74, decayRateTrails: 962, forwardRotationScale: 0.3, - individualTrailWeight: 0.052000000000000005, + individualTrailWeight: 0.052, moveSpeed: 72, sensorOffsetAngle: 42, sensorOffsetDistance: 54, - spawnPerPixel: 0.15999999999999998, - strokeAngleJitterRadians: 3.1399999999999997, + spawnPerPixel: 0.16, + strokeAngleJitterRadians: 3.14, turnSpeed: 44, - turnWhenLost: 0.9200000000000002, + turnWhenLost: 0.92, }, audio: { ...defaultGardenAudioVibeSettings, idleIntensity: 0.13, bpm: 68, rampUpIntensity: 1.46, - rampUpTime: 0.10000000000000002, + rampUpTime: 0.1, noteLength: 0.6, notePitchOffset: -3, brightness: 1.21, @@ -307,7 +307,7 @@ export const vibePresets: Array = [ moveSpeed: 28, sensorOffsetAngle: 34, sensorOffsetDistance: 66, - spawnPerPixel: 0.05499999999999998, + spawnPerPixel: 0.055, strokeAngleJitterRadians: 0, turnSpeed: 30, turnWhenLost: 1.52, @@ -317,7 +317,7 @@ export const vibePresets: Array = [ idleIntensity: 0.33, bpm: 127, rampUpIntensity: 0.66, - rampUpTime: 0.03000000000000001, + rampUpTime: 0.03, noteLength: 0.92, notePitchOffset: 10, brightness: 1.42, @@ -341,7 +341,7 @@ export const vibePresets: Array = [ clarity: 0.1, decayRateTrails: 922, forwardRotationScale: 0.5, - individualTrailWeight: 0.026000000000000002, + individualTrailWeight: 0.026, moveSpeed: 86, sensorOffsetAngle: 46, sensorOffsetDistance: 14, @@ -355,7 +355,7 @@ export const vibePresets: Array = [ idleIntensity: 0.11, bpm: 150, rampUpIntensity: 2, - rampUpTime: 0.06000000000000001, + rampUpTime: 0.06, noteLength: 1.8, notePitchOffset: -12, brightness: 0.5, diff --git a/src/consts.ts b/src/consts.ts deleted file mode 100644 index 8991f08..0000000 --- a/src/consts.ts +++ /dev/null @@ -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; diff --git a/src/game-loop/frame-performance.ts b/src/game-loop/frame-performance.ts index abdb795..5cab70d 100644 --- a/src/game-loop/frame-performance.ts +++ b/src/game-loop/frame-performance.ts @@ -1,19 +1,18 @@ import { settings } from '../settings'; -export class FramePerformance { - private readonly adaptiveRefreshTargetFps = 60; - private readonly initialFps = this.adaptiveRefreshTargetFps; - public smoothedFps = this.initialFps; +const ADAPTIVE_REFRESH_TARGET_FPS = 60; +const ADAPTIVE_CAP_DECREASE_AGENTS_PER_SECOND = 200_000; +const FRAME_GAP_RESET_SECONDS = 1; +const FPS_HEADROOM = 0.9; +const FPS_SMOOTHING_NEW = 0.06; +const FPS_SMOOTHING_RETAIN = 1 - FPS_SMOOTHING_NEW; +export class FramePerformance { + public smoothedFps = ADAPTIVE_REFRESH_TARGET_FPS; public measuredFps = 0; public frameDeltaSeconds = 0; public measuredFrameTimeMs = 0; - private readonly adaptiveCapDecreaseAgentsPerSecond = 200_000; - private readonly frameGapResetSeconds = 1; - private readonly fpsHeadroom = 0.9; - private readonly fpsSmoothingNew = 0.06; - private readonly fpsSmoothingRetain = 1 - this.fpsSmoothingNew; private previousFrameTime: DOMHighResTimeStamp | null = null; public get adaptiveCapInitial(): number { @@ -25,13 +24,13 @@ export class FramePerformance { } public get hasAdaptiveCapHeadroom(): boolean { - return this.smoothedFps >= this.adaptiveRefreshTargetFps * this.fpsHeadroom; + return this.smoothedFps >= ADAPTIVE_REFRESH_TARGET_FPS * FPS_HEADROOM; } public get adaptiveCapDecreaseAgents(): number { return Math.max( 1, - Math.ceil(this.adaptiveCapDecreaseAgentsPerSecond * this.frameDeltaSeconds) + Math.ceil(ADAPTIVE_CAP_DECREASE_AGENTS_PER_SECOND * this.frameDeltaSeconds) ); } @@ -51,11 +50,10 @@ export class FramePerformance { this.frameDeltaSeconds = deltaSeconds; this.measuredFrameTimeMs = deltaSeconds * 1000; this.measuredFps = fps; - if (deltaSeconds > this.frameGapResetSeconds) { + if (deltaSeconds > FRAME_GAP_RESET_SECONDS) { return; } - this.smoothedFps = - this.smoothedFps * this.fpsSmoothingRetain + fps * this.fpsSmoothingNew; + this.smoothedFps = this.smoothedFps * FPS_SMOOTHING_RETAIN + fps * FPS_SMOOTHING_NEW; } } diff --git a/src/game-loop/simulation-textures.ts b/src/game-loop/simulation-textures.ts index 7832469..d2256d0 100644 --- a/src/game-loop/simulation-textures.ts +++ b/src/game-loop/simulation-textures.ts @@ -14,8 +14,7 @@ export class SimulationTextures { public trailMapA: ResizableTexture; public trailMapB: ResizableTexture; // Per-frame deposit accumulator: cleared each frame, written sparsely by - // agents, then read by diffuse alongside trailMapA. Replaces the previous - // full-resolution copyTrailMapAToB seed. + // agents, then read by diffuse alongside trailMapA. public readonly depositMap: ResizableTexture; public readonly eraserMask: ResizableTexture; public sourceMapA: ResizableTexture; diff --git a/src/page/audio-control.ts b/src/page/audio-control.ts index c96fb1d..7677bad 100644 --- a/src/page/audio-control.ts +++ b/src/page/audio-control.ts @@ -1,5 +1,4 @@ import { appConfig } from '../config'; -import { DISABLED_FLAG_VALUE, ENABLED_FLAG_VALUE } from '../consts'; import type GameLoop from '../game-loop/game-loop'; import { readBrowserStorage, writeBrowserStorage } from '../utils/browser-storage'; import { queryRequiredElement } from '../utils/dom'; @@ -21,6 +20,9 @@ const readInitialAudioVolume = (): number => { const formatStoredAudioVolume = (volume: number): string => clampAudioVolume(volume).toFixed(2); +const STORED_MUTED_TRUE = '1'; +const STORED_MUTED_FALSE = '0'; + interface AudioControlOptions { getGame: () => GameLoop | null; hasStarted: () => boolean; @@ -43,7 +45,7 @@ export class AudioControl { private audioVolume = readInitialAudioVolume(); private isMutedState = - readBrowserStorage(appConfig.storage.audioMutedKey) === ENABLED_FLAG_VALUE || + readBrowserStorage(appConfig.storage.audioMutedKey) === STORED_MUTED_TRUE || this.audioVolume <= 0; public constructor(private readonly options: AudioControlOptions) { @@ -140,7 +142,7 @@ export class AudioControl { private persist(): void { writeBrowserStorage( appConfig.storage.audioMutedKey, - this.isMutedState ? ENABLED_FLAG_VALUE : DISABLED_FLAG_VALUE + this.isMutedState ? STORED_MUTED_TRUE : STORED_MUTED_FALSE ); writeBrowserStorage( appConfig.storage.audioVolumeKey, diff --git a/src/page/splash-screen.ts b/src/page/splash-screen.ts index 50dd211..00d44a4 100644 --- a/src/page/splash-screen.ts +++ b/src/page/splash-screen.ts @@ -30,13 +30,24 @@ export class SplashScreen { public awaitStart(onStart: () => void): Promise { this.startButton.disabled = false; return new Promise((resolve) => { + const onKeyDown = (event: KeyboardEvent) => { + if (event.key !== 'Enter' || event.defaultPrevented) { + return; + } + + event.preventDefault(); + this.startButton.click(); + }; const onClick = () => { this.startButton.removeEventListener('click', onClick); + document.removeEventListener('keydown', onKeyDown); onStart(); this.setVisible(this.splash, false); resolve(); }; + this.startButton.addEventListener('click', onClick); + document.addEventListener('keydown', onKeyDown); }); } diff --git a/src/style/vars.scss b/src/style/vars.scss index 24df21b..319d82e 100644 --- a/src/style/vars.scss +++ b/src/style/vars.scss @@ -1,7 +1,7 @@ :root { --transition-time: 200ms; --transition-time-long: 350ms; - --accent-color: rgb(255, 93, 162); + --accent-color: rgb(255 93 162); --main-color: #aaa; --normal-margin: 2rem; --small-margin: 1rem; diff --git a/vite.config.ts b/vite.config.ts index da6207c..b063e3c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -22,7 +22,6 @@ export default defineConfig(({ command }) => ({ }, server: { host: true, - hmr: false, }, test: { environment: 'node', From c40c5d97dba65654e2ff6cca0a00ed2807dd3a01 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 24 May 2026 10:52:20 +0100 Subject: [PATCH 5/5] Final clean up --- .forgejo/workflows/deploy.yml | 5 + .nvmrc | 2 +- .prettierrc | 2 +- assets/icons/download.svg | 4 +- assets/icons/info.svg | 2 +- assets/icons/maximize.svg | 4 +- assets/icons/minimize.svg | 4 +- assets/icons/restart.svg | 2 +- assets/icons/settings.svg | 2 +- assets/icons/sound.svg | 2 +- definitions.d.ts | 8 - e2e/app.spec.ts | 68 ++++-- package-lock.json | 12 +- package.json | 2 +- playwright.config.ts | 2 +- src/analytics.ts | 9 +- src/audio/garden-audio-config.ts | 3 +- src/audio/garden-audio-graph.ts | 23 +- src/audio/garden-audio-music.ts | 25 +-- src/audio/garden-audio-types.ts | 2 +- src/audio/garden-audio.ts | 108 +++++++--- src/audio/noise-burst-player.ts | 5 +- src/audio/piano-sampler.ts | 5 +- src/audio/piano-samples.ts | 172 ++++++++++----- src/audio/samples/README.md | 9 + src/config.ts | 18 +- src/config/default-settings.ts | 10 +- src/config/runtime-controls.ts | 14 +- src/config/types.ts | 7 + src/config/vibe-presets.test.ts | 9 +- src/config/vibe-presets.ts | 40 ++-- src/game-loop/agent-population.test.ts | 12 ++ src/game-loop/agent-population.ts | 2 + src/game-loop/frame-performance.ts | 6 +- src/game-loop/game-loop-resources.ts | 38 ++-- src/game-loop/game-loop.ts | 22 +- src/game-loop/intro-title-agents.ts | 55 +++-- src/game-loop/perf-stats-overlay.ts | 8 +- src/game-loop/pointer-input.ts | 16 +- src/game-loop/simulation-frame.ts | 2 +- src/game-loop/simulation-textures.ts | 2 +- src/game-loop/toolbar-contrast-monitor.ts | 191 +++++++++++------ src/index.ts | 8 +- src/page/collapsible-panel-animator.ts | 29 ++- src/page/config-pane.ts | 199 +----------------- src/page/eraser-size-control.ts | 28 ++- src/page/full-screen-handler.ts | 31 ++- src/page/menu-hider.ts | 8 +- src/page/mirror-segment-control.ts | 2 +- src/page/palette-control.ts | 16 +- src/page/vibe-navigator.ts | 22 +- .../agent-generation/agent-compaction.wgsl | 4 +- .../agent-generation-pipeline.ts | 11 +- src/pipelines/agents/agent-pipeline.ts | 8 +- src/pipelines/agents/agent.wgsl | 8 +- src/pipelines/brush/brush-pipeline.ts | 14 +- src/pipelines/brush/brush.wgsl | 2 +- src/pipelines/diffusion/diffuse.wgsl | 4 +- src/pipelines/diffusion/diffusion-pipeline.ts | 8 +- src/pipelines/eraser/eraser-agent-pipeline.ts | 2 +- src/pipelines/eraser/eraser-agent.wgsl | 2 +- src/pipelines/render/render-pipeline.ts | 11 +- src/pipelines/render/render.wgsl | 6 +- src/settings.ts | 10 +- src/style/_config-pane.scss | 4 +- src/style/mixins.scss | 6 + src/style/toolbar/_layout.scss | 44 +++- src/style/toolbar/_responsive.scss | 10 +- src/utils/error-handler.ts | 7 +- src/utils/graphics/bind-group-cache.ts | 76 +++---- src/vibe-uri.test.ts | 4 +- src/vibe-uri.ts | 8 +- src/vibes.ts | 6 +- tsconfig.json | 2 +- 74 files changed, 864 insertions(+), 670 deletions(-) delete mode 100644 definitions.d.ts diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml index 3bb5949..bbf03a4 100644 --- a/.forgejo/workflows/deploy.yml +++ b/.forgejo/workflows/deploy.yml @@ -32,6 +32,7 @@ jobs: - name: Test run: | npm run lint:check + npm run format:check npm run typecheck npm run typecheck:e2e npm test @@ -40,6 +41,10 @@ jobs: run: | npm run test:e2e + - name: Build + run: | + npm run build + - name: Upload Playwright report if: failure() uses: actions/upload-artifact@v4 diff --git a/.nvmrc b/.nvmrc index 2bd5a0a..6fa8dec 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22 +22.13.0 diff --git a/.prettierrc b/.prettierrc index 743851d..fef36af 100644 --- a/.prettierrc +++ b/.prettierrc @@ -6,5 +6,5 @@ "endOfLine": "lf", "plugins": ["@ianvs/prettier-plugin-sort-imports"], "importOrder": ["", "", "", "^[./]"], - "importOrderTypeScriptVersion": "5.6.0" + "importOrderTypeScriptVersion": "6.0.3" } diff --git a/assets/icons/download.svg b/assets/icons/download.svg index 423bc7d..eb27c41 100644 --- a/assets/icons/download.svg +++ b/assets/icons/download.svg @@ -2,9 +2,9 @@ diff --git a/assets/icons/info.svg b/assets/icons/info.svg index 3573e28..19e44e5 100644 --- a/assets/icons/info.svg +++ b/assets/icons/info.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/maximize.svg b/assets/icons/maximize.svg index 7239da3..bf1f311 100644 --- a/assets/icons/maximize.svg +++ b/assets/icons/maximize.svg @@ -1,7 +1,7 @@ - + - \ No newline at end of file + diff --git a/assets/icons/minimize.svg b/assets/icons/minimize.svg index 2ffe1f7..19e8e7f 100644 --- a/assets/icons/minimize.svg +++ b/assets/icons/minimize.svg @@ -1,7 +1,7 @@ - + - \ No newline at end of file + diff --git a/assets/icons/restart.svg b/assets/icons/restart.svg index a58d2a6..d3dce2c 100644 --- a/assets/icons/restart.svg +++ b/assets/icons/restart.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/settings.svg b/assets/icons/settings.svg index ba7478f..9d5e6fd 100644 --- a/assets/icons/settings.svg +++ b/assets/icons/settings.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/sound.svg b/assets/icons/sound.svg index d440a9c..4f382b5 100644 --- a/assets/icons/sound.svg +++ b/assets/icons/sound.svg @@ -1,3 +1,3 @@ - + diff --git a/definitions.d.ts b/definitions.d.ts deleted file mode 100644 index c90ad44..0000000 --- a/definitions.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -declare module '*.wgsl?raw' { - const content: string; - export default content; -} - -interface HTMLCanvasElement { - getContext(contextId: 'webgpu'): GPUCanvasContext | null; -} diff --git a/e2e/app.spec.ts b/e2e/app.spec.ts index 3285b30..1caa3f2 100644 --- a/e2e/app.spec.ts +++ b/e2e/app.spec.ts @@ -1,7 +1,12 @@ -import { expect, test, type Page } from '@playwright/test'; +import { test as base, expect, type Page } from '@playwright/test'; const canvasName = 'Interactive generative garden canvas'; +interface BrowserDiagnostics { + browserFailures: Array; + consoleErrors: Array; +} + const isLocalUrl = (url: string) => { const { hostname } = new URL(url); return hostname === '127.0.0.1' || hostname === 'localhost'; @@ -29,6 +34,27 @@ const collectLocalBrowserFailures = (page: Page) => { return failures; }; +const test = base.extend<{ browserDiagnostics: BrowserDiagnostics }>({ + browserDiagnostics: [ + async ({ page }, use) => { + const browserFailures = collectLocalBrowserFailures(page); + const consoleErrors: Array = []; + + page.on('console', (message) => { + if (message.type() === 'error') { + consoleErrors.push(message.text()); + } + }); + + await use({ browserFailures, consoleErrors }); + + expect(consoleErrors).toEqual([]); + expect(browserFailures).toEqual([]); + }, + { auto: true }, + ], +}); + const disableWebGpu = async (page: Page) => { await page.addInitScript(() => { Object.defineProperty(navigator, 'gpu', { @@ -39,14 +65,6 @@ const disableWebGpu = async (page: Page) => { }; test('starts the WebGPU garden and accepts drawing input', async ({ page }) => { - const browserFailures = collectLocalBrowserFailures(page); - const consoleErrors: Array = []; - page.on('console', (message) => { - if (message.type() === 'error') { - consoleErrors.push(message.text()); - } - }); - await page.addInitScript((expectedCanvasName) => { const captureState = { count: 0 }; Object.defineProperty(window, '__fleetingGardenPointerCaptures', { @@ -68,7 +86,7 @@ test('starts the WebGPU garden and accepts drawing input', async ({ page }) => { }, canvasName); await page.goto('/'); - const startButton = page.getByRole('button', { name: 'Start' }); + const startButton = page.getByRole('button', { exact: true, name: 'Start' }); await expect(startButton).toBeVisible(); await expect(startButton).toBeEnabled({ timeout: 30_000 }); await page.keyboard.press('Enter'); @@ -117,13 +135,21 @@ test('starts the WebGPU garden and accepts drawing input', async ({ page }) => { ) .toBeGreaterThan(0); - expect(consoleErrors).toEqual([]); - expect(browserFailures).toEqual([]); + await expect + .poll(() => + page.evaluate( + () => + ( + window as unknown as { + __fleetingGardenBrushPasses?: number; + } + ).__fleetingGardenBrushPasses ?? 0 + ) + ) + .toBeGreaterThan(0); }); test('shows a clear fallback when WebGPU is unavailable', async ({ page }) => { - const browserFailures = collectLocalBrowserFailures(page); - await disableWebGpu(page); await page.goto('/'); @@ -135,23 +161,19 @@ test('shows a clear fallback when WebGPU is unavailable', async ({ page }) => { const fallback = page.getByRole('alert'); await expect(fallback).toContainText('Fleeting Garden needs WebGPU'); await expect(fallback).toContainText('webgpu-unsupported'); - expect(browserFailures).toEqual([]); }); test('syncs the selected vibe with the URI', async ({ page }) => { - const browserFailures = collectLocalBrowserFailures(page); - await disableWebGpu(page); - await page.goto('/?vibe=Bone%20Archive'); + await page.goto('/?vibe=Aurora%20Mycelium'); - await expect(page).toHaveURL(/vibe=bone-archive/); + await expect(page).toHaveURL(/vibe=aurora-mycelium/); await page.getByRole('button', { name: 'Next vibe' }).click(); - await expect(page).toHaveURL(/vibe=pelagic-caustics/); + await expect(page).toHaveURL(/vibe=velvet-observatory/); await page.goBack(); - await expect(page).toHaveURL(/vibe=bone-archive/); - expect(browserFailures).toEqual([]); + await expect(page).toHaveURL(/vibe=aurora-mycelium/); }); test('keeps audio focus outlines scoped to the active control', async ({ page }) => { @@ -194,7 +216,7 @@ test('keeps the config overlay scrollable and dismissible on mobile', async ({ await page.setViewportSize({ width: 390, height: 640 }); await page.goto('/'); - const startButton = page.getByRole('button', { name: 'Start' }); + const startButton = page.getByRole('button', { exact: true, name: 'Start' }); await expect(startButton).toBeEnabled({ timeout: 30_000 }); await startButton.click(); await expect(page.locator('body')).not.toHaveClass(/is-loading/, { diff --git a/package-lock.json b/package-lock.json index 89cdaff..7c0cc30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,13 +10,13 @@ "license": "Unlicense", "dependencies": { "@plausible-analytics/tracker": "^0.4.5", - "tweakpane": "^4.0.5" + "tweakpane": "~4.0.5" }, "devDependencies": { "@eslint/js": "^10.0.1", "@ianvs/prettier-plugin-sort-imports": "^4.7.1", "@playwright/test": "^1.60.0", - "@tweakpane/core": "^2.0.5", + "@tweakpane/core": "~2.0.5", "@types/node": "^25.6.0", "@vite-pwa/assets-generator": "^1.0.2", "@vitejs/plugin-basic-ssl": "^2.3.0", @@ -38,7 +38,7 @@ "vitest": "^4.1.5" }, "engines": { - "node": ">=20" + "node": ">=22.13.0" } }, "node_modules/@babel/code-frame": { @@ -2774,9 +2774,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index fdd05c4..a65ec2a 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "generate-icons": "pwa-assets-generator" }, "engines": { - "node": ">=22" + "node": ">=22.13.0" }, "repository": { "type": "git", diff --git a/playwright.config.ts b/playwright.config.ts index e62296a..01f7ce9 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ fullyParallel: true, forbidOnly: isCi, retries: isCi ? 2 : 0, - workers: isCi ? 1 : undefined, + workers: 1, reporter: isCi ? [['list'], ['html', { open: 'never' }]] : 'list', use: { baseURL, diff --git a/src/analytics.ts b/src/analytics.ts index ddfb650..b19958a 100644 --- a/src/analytics.ts +++ b/src/analytics.ts @@ -4,6 +4,7 @@ import { type PlausibleEventOptions, } from '@plausible-analytics/tracker'; +import { appConfig } from './config'; import type { VibeId } from './vibes'; let isInitialized = false; @@ -23,10 +24,10 @@ export const initAnalytics = () => { try { plausibleInit({ - domain: 'schmelczer.dev/floating', - endpoint: 'https://stats.schmelczer.dev/status', - autoCapturePageviews: true, - logging: true, + domain: appConfig.analytics.domain, + endpoint: appConfig.analytics.endpoint, + autoCapturePageviews: appConfig.analytics.autoCapturePageviews, + logging: appConfig.analytics.logging, }); isInitialized = true; } catch (error) { diff --git a/src/audio/garden-audio-config.ts b/src/audio/garden-audio-config.ts index da68c93..b5fbc1d 100644 --- a/src/audio/garden-audio-config.ts +++ b/src/audio/garden-audio-config.ts @@ -1,6 +1,7 @@ import type { PianoNoteRole } from './garden-audio-types'; -const DEFAULT_AUDIO_VOLUME = 0.5; +export const DEFAULT_AUDIO_VOLUME = 0.5; +export const SILENT_AUDIO_GAIN = 0.0001; type GardenAudioChordQuality = 'major' | 'minor' | 'sus2' | 'sus4'; diff --git a/src/audio/garden-audio-graph.ts b/src/audio/garden-audio-graph.ts index de934f9..a288465 100644 --- a/src/audio/garden-audio-graph.ts +++ b/src/audio/garden-audio-graph.ts @@ -1,7 +1,9 @@ import { clamp } from '../utils/math'; -import type { GardenAudioConfig } from './garden-audio-config'; +import { SILENT_AUDIO_GAIN, type GardenAudioConfig } from './garden-audio-config'; import type { PianoNoteRole } from './garden-audio-types'; +type AudioSessionType = NonNullable['type']; + type NavigatorWithAudioSession = Navigator & { audioSession?: { type: @@ -17,7 +19,7 @@ type NavigatorWithAudioSession = Navigator & { const outputHighPassFrequencyHz = 45; const noiseBufferDurationSeconds = 1; const graphTuning = { - closeGain: 0.0001, + closeGain: SILENT_AUDIO_GAIN, closeRampSeconds: 0.015, delayMaxSeconds: 2, eventBusGain: 1, @@ -54,6 +56,7 @@ export class GardenAudioGraph { private pianoBusGainScale = 1; private pianoBusGainScaleAutomationUntil = 0; private pianoBusGainScaleTimeConstantSeconds = 0; + private previousAudioSessionType: AudioSessionType | null = null; private readonly pianoBuses = new Map(); public constructor(private readonly config: GardenAudioConfig) {} @@ -77,6 +80,7 @@ export class GardenAudioGraph { // Audio Session API. const audioSession = (navigator as NavigatorWithAudioSession).audioSession; if (audioSession) { + this.previousAudioSessionType ??= audioSession.type; audioSession.type = 'playback'; } @@ -203,6 +207,21 @@ export class GardenAudioGraph { if (context.state !== 'closed') { await context.close().catch(() => undefined); } + + this.restoreAudioSessionType(); + } + + private restoreAudioSessionType(): void { + const previousType = this.previousAudioSessionType; + this.previousAudioSessionType = null; + if (previousType === null) { + return; + } + + const audioSession = (navigator as NavigatorWithAudioSession).audioSession; + if (audioSession) { + audioSession.type = previousType; + } } private createDelay(context: AudioContext, masterGain: GainNode): void { diff --git a/src/audio/garden-audio-music.ts b/src/audio/garden-audio-music.ts index 0d056e7..a0e5e7b 100644 --- a/src/audio/garden-audio-music.ts +++ b/src/audio/garden-audio-music.ts @@ -13,8 +13,6 @@ const DEFAULT_PROGRESSION: ReadonlyArray = [ const DEFAULT_ROOT_MIDI = 57; const DEFAULT_SCALE: ReadonlyArray = [0, 2, 4, 7, 9]; -const profileCache = new WeakMap(); - const getProfileScale = (vibe: VibePreset): Array => { const scale = vibe.audio.scale?.length ? vibe.audio.scale : DEFAULT_SCALE; return [...scale]; @@ -26,21 +24,10 @@ const getProfileProgression = (vibe: VibePreset): Array => ); export const getVibeProfile = (vibe: VibePreset): GardenAudioVibeProfile => { - let profile = profileCache.get(vibe); - if (!profile) { - profile = { - ...vibe.audio, - rootMidi: DEFAULT_ROOT_MIDI + vibe.audio.notePitchOffset, - scale: getProfileScale(vibe), - progression: getProfileProgression(vibe), - }; - profileCache.set(vibe, profile); - return profile; - } - - Object.assign(profile, vibe.audio); - profile.rootMidi = DEFAULT_ROOT_MIDI + vibe.audio.notePitchOffset; - profile.scale = getProfileScale(vibe); - profile.progression = getProfileProgression(vibe); - return profile; + return { + ...vibe.audio, + rootMidi: DEFAULT_ROOT_MIDI + vibe.audio.notePitchOffset, + scale: getProfileScale(vibe), + progression: getProfileProgression(vibe), + }; }; diff --git a/src/audio/garden-audio-types.ts b/src/audio/garden-audio-types.ts index 3a0a592..fecbcf8 100644 --- a/src/audio/garden-audio-types.ts +++ b/src/audio/garden-audio-types.ts @@ -1,4 +1,4 @@ -import { VibePreset } from '../vibes'; +import type { VibePreset } from '../vibes'; export interface GardenAudioSnapshot { vibe: VibePreset; diff --git a/src/audio/garden-audio.ts b/src/audio/garden-audio.ts index 5d6131c..3aa4c88 100644 --- a/src/audio/garden-audio.ts +++ b/src/audio/garden-audio.ts @@ -1,7 +1,11 @@ import { ErrorHandler, Severity } from '../utils/error-handler'; import { clamp01 } from '../utils/math'; import type { VibeId, VibePreset } from '../vibes'; -import type { GardenAudioConfig, GardenAudioVibeProfile } from './garden-audio-config'; +import { + SILENT_AUDIO_GAIN, + type GardenAudioConfig, + type GardenAudioVibeProfile, +} from './garden-audio-config'; import { GardenAudioEnergy } from './garden-audio-energy'; import { GardenAudioGestureState } from './garden-audio-gesture-state'; import { GardenAudioGraph } from './garden-audio-graph'; @@ -13,8 +17,12 @@ import { NoiseBurstPlayer } from './noise-burst-player'; import { PianoSampler } from './piano-sampler'; type AudioLifecycle = 'idle' | 'started' | 'destroyed'; +type PianoReleasePhase = + | { kind: 'idle' } + | { kind: 'awaiting-fade' } + | { kind: 'scheduled-fade'; fadeAt: number } + | { kind: 'settling'; stopAt: number }; -const muteGain = 0.0001; const muteRampSeconds = 0.02; const brushUpPianoBusFadeSeconds = 2.4; const brushUpPianoBusFadeSettleSeconds = 3.2; @@ -29,16 +37,16 @@ export class GardenAudio { private readonly pianoEngine: GenerativePianoEngine; private currentVibeId: VibeId | null = null; + private currentVibe: VibePreset | null = null; private lifecycle: AudioLifecycle = 'idle'; - private isReleasingPiano = false; + private pianoReleasePhase: PianoReleasePhase = { kind: 'idle' }; private isMuted = false; private isGestureActive = false; - private fadePianoAt: number | null = null; private masterVolume: number; - private stopPianoAt: number | null = null; private lastEraserAt = Number.NEGATIVE_INFINITY; private lastVibeStingerAt = Number.NEGATIVE_INFINITY; private startRequestId = 0; + private hasLoadedPiano = false; public constructor(private readonly config: GardenAudioConfig) { this.masterVolume = clamp01(config.masterVolume); @@ -60,7 +68,8 @@ export class GardenAudio { if ( this.lifecycle === 'started' && this.currentVibeId === vibe.id && - this.graph.context?.state === 'running' + this.graph.context?.state === 'running' && + this.hasLoadedPiano ) { return; } @@ -74,6 +83,7 @@ export class GardenAudio { ? muteRampSeconds : this.config.fadeInSeconds; const needsResume = context.state !== 'running' && context.state !== 'closed'; + const startRequestId = ++this.startRequestId; if (needsResume) { if (!isUserGesture) { @@ -83,7 +93,7 @@ export class GardenAudio { .resume() .then(() => { if (this.graph.context === context && this.lifecycle !== 'destroyed') { - this.completeStart(vibe, { context, startupRampSeconds }); + this.completeStart(vibe, { context, startupRampSeconds, startRequestId }); } }) .catch((error) => { @@ -95,16 +105,18 @@ export class GardenAudio { return; } - this.completeStart(vibe, { context, startupRampSeconds }); + this.completeStart(vibe, { context, startupRampSeconds, startRequestId }); } private completeStart( vibe: VibePreset, { context, + startRequestId, startupRampSeconds, }: { context: AudioContext; + startRequestId: number; startupRampSeconds: number; } ): void { @@ -113,11 +125,11 @@ export class GardenAudio { } if (this.isMuted) { - this.graph.setMasterGain(muteGain, muteRampSeconds); + this.activateMutedStart(vibe, context); + this.graph.setMasterGain(SILENT_AUDIO_GAIN, muteRampSeconds); return; } - const startRequestId = ++this.startRequestId; void this.piano .load(context) .then(() => { @@ -155,15 +167,28 @@ export class GardenAudio { ): void { this.lifecycle = 'started'; this.currentVibeId = vibe.id; + this.currentVibe = vibe; const profile = getVibeProfile(vibe); this.graph.applyDelayProfile(profile.bpm); this.graph.setMasterGain(this.masterVolume, startupRampSeconds); if (cuePiano) { + this.hasLoadedPiano = true; this.pianoEngine.cue(context.currentTime, profile); } } + private activateMutedStart(vibe: VibePreset, context: AudioContext): void { + this.lifecycle = 'started'; + this.currentVibeId = vibe.id; + this.currentVibe = vibe; + this.hasLoadedPiano = false; + this.graph.applyDelayProfile(getVibeProfile(vibe).bpm); + if (this.graph.context === context) { + this.pianoEngine.reset(); + } + } + public changeVibe(vibe: VibePreset, options: { userGesture?: boolean } = {}): void { const previousVibeId = this.currentVibeId; this.start(vibe, options); @@ -171,6 +196,7 @@ export class GardenAudio { if (didChangeVibe) { this.piano.stopAll(); + this.hasLoadedPiano = false; } const context = this.graph.context; @@ -192,9 +218,13 @@ export class GardenAudio { this.isMuted = isMuted; this.graph.setMasterGain( - isMuted ? muteGain : this.masterVolume, + isMuted ? SILENT_AUDIO_GAIN : this.masterVolume, isMuted ? muteRampSeconds : this.config.fadeInSeconds ); + + if (!isMuted && this.currentVibe && !this.hasLoadedPiano) { + this.start(this.currentVibe); + } } public setMasterVolume(masterVolume: number): void { @@ -211,9 +241,7 @@ export class GardenAudio { } this.isGestureActive = true; - this.isReleasingPiano = false; - this.fadePianoAt = null; - this.stopPianoAt = null; + this.pianoReleasePhase = { kind: 'idle' }; this.graph.setPianoBusGainScale(1, this.config.fadeInSeconds); this.gestureState.reset(); this.energy.beginGesture(context.currentTime); @@ -223,9 +251,7 @@ export class GardenAudio { public endGesture(): void { this.gestureState.reset(); this.isGestureActive = false; - this.isReleasingPiano = true; - this.fadePianoAt = null; - this.stopPianoAt = null; + this.pianoReleasePhase = { kind: 'awaiting-fade' }; this.energy.endGesture(); this.pianoEngine.endGesture(); } @@ -244,7 +270,7 @@ export class GardenAudio { this.energy.silence(); } - if (!this.isGestureActive && this.isReleasingPiano) { + if (!this.isGestureActive && this.pianoReleasePhase.kind !== 'idle') { this.updatePianoRelease(snapshot.vibe, context.currentTime); this.updateDelay(snapshot, profile); return; @@ -299,14 +325,14 @@ export class GardenAudio { await this.graph.close(); this.piano.reset(); + this.hasLoadedPiano = false; this.energy.reset(); this.gestureState.reset(); this.pianoEngine.reset(); this.currentVibeId = null; + this.currentVibe = null; this.isGestureActive = false; - this.isReleasingPiano = false; - this.fadePianoAt = null; - this.stopPianoAt = null; + this.pianoReleasePhase = { kind: 'idle' }; this.lastEraserAt = Number.NEGATIVE_INFINITY; this.lastVibeStingerAt = Number.NEGATIVE_INFINITY; } @@ -327,21 +353,41 @@ export class GardenAudio { } private updatePianoRelease(vibe: VibePreset, now: number): void { - if (this.fadePianoAt === null && this.stopPianoAt === null) { - this.fadePianoAt = this.pianoEngine.release(vibe, now); - } + if (this.pianoReleasePhase.kind === 'awaiting-fade') { + const fadeAt = this.pianoEngine.release(vibe, now); + if (now < fadeAt) { + this.pianoReleasePhase = { kind: 'scheduled-fade', fadeAt }; + return; + } - if (this.fadePianoAt !== null && now >= this.fadePianoAt) { this.graph.setPianoBusGainScale(0, brushUpPianoBusFadeSeconds); - this.fadePianoAt = null; - this.stopPianoAt = now + brushUpPianoBusFadeSettleSeconds; + this.pianoReleasePhase = { + kind: 'settling', + stopAt: now + brushUpPianoBusFadeSettleSeconds, + }; + return; } - if (this.stopPianoAt !== null && now >= this.stopPianoAt) { + if ( + this.pianoReleasePhase.kind === 'scheduled-fade' && + now >= this.pianoReleasePhase.fadeAt + ) { + this.graph.setPianoBusGainScale(0, brushUpPianoBusFadeSeconds); + this.pianoReleasePhase = { + kind: 'settling', + stopAt: now + brushUpPianoBusFadeSettleSeconds, + }; + return; + } + + if ( + this.pianoReleasePhase.kind === 'settling' && + now >= this.pianoReleasePhase.stopAt + ) { this.piano.stopAll(); this.pianoEngine.reset(); - this.stopPianoAt = null; - this.isReleasingPiano = false; + this.hasLoadedPiano = false; + this.pianoReleasePhase = { kind: 'idle' }; } } @@ -393,8 +439,10 @@ export class GardenAudio { } this.currentVibeId = vibe.id; + this.currentVibe = vibe; const profile = getVibeProfile(vibe); this.graph.applyDelayProfile(profile.bpm); this.pianoEngine.cue(this.graph.context.currentTime, profile); + this.hasLoadedPiano = true; } } diff --git a/src/audio/noise-burst-player.ts b/src/audio/noise-burst-player.ts index 7d148ac..f9ab2bf 100644 --- a/src/audio/noise-burst-player.ts +++ b/src/audio/noise-burst-player.ts @@ -45,7 +45,10 @@ export class NoiseBurstPlayer { filter.connect(envelope); envelope.connect(panner); panner.connect(noiseBus); - source.start(scheduledStart, Math.random() * noiseBurstTuning.offsetRandomSeconds); + const maxOffsetSeconds = Math.max(0, noiseBuffer.duration - durationSeconds); + const offsetSeconds = + Math.random() * Math.min(noiseBurstTuning.offsetRandomSeconds, maxOffsetSeconds); + source.start(scheduledStart, offsetSeconds); source.stop(stopAt); source.addEventListener( 'ended', diff --git a/src/audio/piano-sampler.ts b/src/audio/piano-sampler.ts index a3f80f7..1a0c77c 100644 --- a/src/audio/piano-sampler.ts +++ b/src/audio/piano-sampler.ts @@ -19,6 +19,7 @@ const pianoSamplerTuning = { minDurationSeconds: 0.08, minFadeSeconds: 0.08, minGain: 0.0001, + releaseTimeConstantCount: 5, tailStopExtraSeconds: 0.05, voiceStealFadeSeconds: 0.025, voiceStealStopSeconds: 0.05, @@ -84,7 +85,9 @@ export class PianoSampler { const sustainAt = scheduledStart + Math.max(pianoSamplerTuning.minDurationSeconds, durationSeconds); const releaseAt = sustainAt + sustainSeconds; - const stopAt = releaseAt + this.config.piano.releaseSeconds; + const stopAt = + releaseAt + + this.config.piano.releaseSeconds * pianoSamplerTuning.releaseTimeConstantCount; const source = context.createBufferSource(); source.buffer = sample.buffer; diff --git a/src/audio/piano-samples.ts b/src/audio/piano-samples.ts index 8b3b6c4..569eca4 100644 --- a/src/audio/piano-samples.ts +++ b/src/audio/piano-samples.ts @@ -31,51 +31,56 @@ import fSharp6SampleUrl from './samples/Fsharp6v12.m4a?url&no-inline'; import fSharp7SampleUrl from './samples/Fsharp7v12.m4a?url&no-inline'; interface PianoSampleDefinition { - midi: number; - path: string; + note: string; url: string; } export interface PianoSampleLoadProgress { + failedCount: number; loadedCount: number; + settledCount: number; totalCount: number; } const pianoSampleDefinitions: Array = [ - { url: a0SampleUrl, path: './samples/A0v12.m4a', midi: 21 }, - { url: c1SampleUrl, path: './samples/C1v12.m4a', midi: 24 }, - { url: dSharp1SampleUrl, path: './samples/Dsharp1v12.m4a', midi: 27 }, - { url: fSharp1SampleUrl, path: './samples/Fsharp1v12.m4a', midi: 30 }, - { url: a1SampleUrl, path: './samples/A1v12.m4a', midi: 33 }, - { url: c2SampleUrl, path: './samples/C2v12.m4a', midi: 36 }, - { url: dSharp2SampleUrl, path: './samples/Dsharp2v12.m4a', midi: 39 }, - { url: fSharp2SampleUrl, path: './samples/Fsharp2v12.m4a', midi: 42 }, - { url: a2SampleUrl, path: './samples/A2v12.m4a', midi: 45 }, - { url: c3SampleUrl, path: './samples/C3v12.m4a', midi: 48 }, - { url: dSharp3SampleUrl, path: './samples/Dsharp3v12.m4a', midi: 51 }, - { url: fSharp3SampleUrl, path: './samples/Fsharp3v12.m4a', midi: 54 }, - { url: a3SampleUrl, path: './samples/A3v12.m4a', midi: 57 }, - { url: c4SampleUrl, path: './samples/C4v12.m4a', midi: 60 }, - { url: dSharp4SampleUrl, path: './samples/Dsharp4v12.m4a', midi: 63 }, - { url: fSharp4SampleUrl, path: './samples/Fsharp4v12.m4a', midi: 66 }, - { url: a4SampleUrl, path: './samples/A4v12.m4a', midi: 69 }, - { url: c5SampleUrl, path: './samples/C5v12.m4a', midi: 72 }, - { url: dSharp5SampleUrl, path: './samples/Dsharp5v12.m4a', midi: 75 }, - { url: fSharp5SampleUrl, path: './samples/Fsharp5v12.m4a', midi: 78 }, - { url: a5SampleUrl, path: './samples/A5v12.m4a', midi: 81 }, - { url: c6SampleUrl, path: './samples/C6v12.m4a', midi: 84 }, - { url: dSharp6SampleUrl, path: './samples/Dsharp6v12.m4a', midi: 87 }, - { url: fSharp6SampleUrl, path: './samples/Fsharp6v12.m4a', midi: 90 }, - { url: a6SampleUrl, path: './samples/A6v12.m4a', midi: 93 }, - { url: c7SampleUrl, path: './samples/C7v12.m4a', midi: 96 }, - { url: dSharp7SampleUrl, path: './samples/Dsharp7v12.m4a', midi: 99 }, - { url: fSharp7SampleUrl, path: './samples/Fsharp7v12.m4a', midi: 102 }, - { url: a7SampleUrl, path: './samples/A7v12.m4a', midi: 105 }, - { url: c8SampleUrl, path: './samples/C8v12.m4a', midi: 108 }, + { url: a0SampleUrl, note: 'A0' }, + { url: c1SampleUrl, note: 'C1' }, + { url: dSharp1SampleUrl, note: 'Dsharp1' }, + { url: fSharp1SampleUrl, note: 'Fsharp1' }, + { url: a1SampleUrl, note: 'A1' }, + { url: c2SampleUrl, note: 'C2' }, + { url: dSharp2SampleUrl, note: 'Dsharp2' }, + { url: fSharp2SampleUrl, note: 'Fsharp2' }, + { url: a2SampleUrl, note: 'A2' }, + { url: c3SampleUrl, note: 'C3' }, + { url: dSharp3SampleUrl, note: 'Dsharp3' }, + { url: fSharp3SampleUrl, note: 'Fsharp3' }, + { url: a3SampleUrl, note: 'A3' }, + { url: c4SampleUrl, note: 'C4' }, + { url: dSharp4SampleUrl, note: 'Dsharp4' }, + { url: fSharp4SampleUrl, note: 'Fsharp4' }, + { url: a4SampleUrl, note: 'A4' }, + { url: c5SampleUrl, note: 'C5' }, + { url: dSharp5SampleUrl, note: 'Dsharp5' }, + { url: fSharp5SampleUrl, note: 'Fsharp5' }, + { url: a5SampleUrl, note: 'A5' }, + { url: c6SampleUrl, note: 'C6' }, + { url: dSharp6SampleUrl, note: 'Dsharp6' }, + { url: fSharp6SampleUrl, note: 'Fsharp6' }, + { url: a6SampleUrl, note: 'A6' }, + { url: c7SampleUrl, note: 'C7' }, + { url: dSharp7SampleUrl, note: 'Dsharp7' }, + { url: fSharp7SampleUrl, note: 'Fsharp7' }, + { url: a7SampleUrl, note: 'A7' }, + { url: c8SampleUrl, note: 'C8' }, ]; let loadedPianoSamples: Array | null = null; let pianoSampleLoadPromise: Promise> | null = null; +let lastPianoSampleProgress: PianoSampleLoadProgress | null = null; +const pianoSampleProgressListeners = new Set< + (progress: PianoSampleLoadProgress) => void +>(); const sampleLoadTuning = { concurrency: 4, @@ -102,50 +107,65 @@ export const loadPianoSamples = ( decodeContext: BaseAudioContext, onProgress?: (progress: PianoSampleLoadProgress) => void ): Promise> => { + const unsubscribeProgress = subscribeToPianoSampleProgress(onProgress); + if (loadedPianoSamples) { - onProgress?.({ + emitPianoSampleProgress({ + failedCount: 0, loadedCount: loadedPianoSamples.length, + settledCount: loadedPianoSamples.length, totalCount: pianoSampleDefinitions.length, }); + unsubscribeProgress(); return Promise.resolve([...loadedPianoSamples]); } if (pianoSampleLoadPromise) { - return pianoSampleLoadPromise; + return pianoSampleLoadPromise.finally(unsubscribeProgress); } let loadedCount = 0; + let failedCount = 0; + let settledCount = 0; const totalCount = pianoSampleDefinitions.length; - onProgress?.({ loadedCount, totalCount }); + emitPianoSampleProgress({ failedCount, loadedCount, settledCount, totalCount }); pianoSampleLoadPromise = loadPianoSampleBatch( pianoSampleDefinitions, async (sample) => { try { - return await withTimeout( + const loadedSample = await withTimeout( (signal) => loadPianoSample(decodeContext, sample, signal), sampleLoadTuning.sampleTimeoutMs ); - } finally { loadedCount += 1; - onProgress?.({ loadedCount, totalCount }); + return loadedSample; + } catch (error) { + failedCount += 1; + throw error; + } finally { + settledCount += 1; + emitPianoSampleProgress({ failedCount, loadedCount, settledCount, totalCount }); } } - ).then( - (samples) => { - loadedPianoSamples = samples.sort((a, b) => a.midi - b.midi); - if (loadedPianoSamples.length !== pianoSampleDefinitions.length) { - throw new Error( - `Loaded ${loadedPianoSamples.length}/${pianoSampleDefinitions.length} piano samples.` - ); + ) + .then( + (samples) => { + loadedPianoSamples = samples.sort((a, b) => a.midi - b.midi); + if (loadedPianoSamples.length !== pianoSampleDefinitions.length) { + throw new Error( + `Loaded ${loadedPianoSamples.length}/${pianoSampleDefinitions.length} piano samples.` + ); + } + return [...loadedPianoSamples]; + }, + (error: unknown) => { + pianoSampleLoadPromise = null; + pianoSampleProgressListeners.clear(); + throw error; } - return [...loadedPianoSamples]; - }, - (error: unknown) => { - pianoSampleLoadPromise = null; - throw error; - } - ); + ) + .finally(unsubscribeProgress); return pianoSampleLoadPromise; }; @@ -160,12 +180,12 @@ const loadPianoSample = async ( ): Promise => { const response = await fetch(sample.url, { signal }); if (!response.ok) { - throw new Error(`Unable to load piano sample ${sample.path}`); + throw new Error(`Unable to load piano sample ${getPianoSamplePath(sample)}`); } const audioData = await response.arrayBuffer(); const buffer = await decodeContext.decodeAudioData(audioData); - return { midi: sample.midi, buffer }; + return { midi: getMidiForPianoSample(sample), buffer }; }; const loadPianoSampleBatch = async ( @@ -205,3 +225,47 @@ const withTimeout = ( } ); }); + +const subscribeToPianoSampleProgress = ( + onProgress: ((progress: PianoSampleLoadProgress) => void) | undefined +): (() => void) => { + if (!onProgress) { + return () => undefined; + } + + pianoSampleProgressListeners.add(onProgress); + if (lastPianoSampleProgress) { + onProgress(lastPianoSampleProgress); + } + return () => { + pianoSampleProgressListeners.delete(onProgress); + }; +}; + +const emitPianoSampleProgress = (progress: PianoSampleLoadProgress): void => { + lastPianoSampleProgress = progress; + pianoSampleProgressListeners.forEach((listener) => listener(progress)); +}; + +const getPianoSamplePath = (sample: PianoSampleDefinition): string => + `./samples/${sample.note}v12.m4a`; + +const getMidiForPianoSample = (sample: PianoSampleDefinition): number => { + const match = /^(?[A-G])(?sharp)?(?\d+)$/.exec(sample.note); + if (!match?.groups) { + throw new Error(`Invalid piano sample note ${sample.note}`); + } + + const semitoneByName: Record = { + C: 0, + D: 2, + E: 4, + F: 5, + G: 7, + A: 9, + B: 11, + }; + const octave = Number(match.groups.octave); + const semitone = semitoneByName[match.groups.name] + (match.groups.accidental ? 1 : 0); + return (octave + 1) * 12 + semitone; +}; diff --git a/src/audio/samples/README.md b/src/audio/samples/README.md index ab6ee8c..bdde746 100644 --- a/src/audio/samples/README.md +++ b/src/audio/samples/README.md @@ -5,3 +5,12 @@ under CC BY 3.0. Source package: @audio-samples/piano-velocity12 Source recording: https://archive.org/details/SalamanderGrandPianoV3 License: https://creativecommons.org/licenses/by/3.0/ + +Checked-in subset: velocity layer `v12`, every minor-third anchor from A0 +through C8: A, C, Dsharp, and Fsharp for octaves 1-7, plus A0, A7, and C8. +The app derives MIDI values from those note names in `piano-samples.ts`. + +Repro notes: start from the matching `v12` OGG files in the source package and +transcode each selected sample to AAC/M4A without renaming the note/velocity +stem. The expected output filenames are `v12.m4a`, for example +`C4v12.m4a`. diff --git a/src/config.ts b/src/config.ts index 27e8a24..e0e6b19 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,11 +1,12 @@ -import { createGardenAudioConfig } from './audio/garden-audio-config'; +import { + createGardenAudioConfig, + DEFAULT_AUDIO_VOLUME, +} from './audio/garden-audio-config'; import { defaultSettings } from './config/default-settings'; import { runtimeControls } from './config/runtime-controls'; import type { GardenAppConfig } from './config/types'; import { defaultVibeId, vibePresets } from './config/vibe-presets'; -const DEFAULT_AUDIO_VOLUME = 0.5; - export { normalizeNumberControlValue, normalizeRuntimeSettings, @@ -19,6 +20,12 @@ export type { export const appConfig = { audio: createGardenAudioConfig(), + analytics: { + autoCapturePageviews: true, + domain: 'fleeting.garden', + endpoint: 'https://stats.schmelczer.dev/status', + logging: import.meta.env.DEV, + }, deltaTime: { maxDeltaTimeSeconds: 1 / 30, minDeltaTimeSeconds: 1 / 240, @@ -85,13 +92,14 @@ export const appConfig = { letterSpacingEm: 0.07, maskAlphaThreshold: 32, maskGradientThreshold: 8, + maskMaxPixels: 1_000_000, maskSampleDensity: 540, maxHeightRatio: 0.25, maxWidthRatio: 0.76, minEntryJitterPx: 6, minFontSizePx: 18, minTargetJitterPx: 1, - pathEasing: 'easeOutQuad' as GardenAppConfig['simulation']['intro']['pathEasing'], + pathEasing: 'easeOutQuad', pathProgressEpsilon: 0.001, radialJitterRatio: 0.35, radialStartEpsilon: 0.001, @@ -124,7 +132,7 @@ export const appConfig = { controlScaleMax: 1.34, controlScaleMin: 0.74, default: 96, - max: 240, + max: 480, min: 24, step: 1, }, diff --git a/src/config/default-settings.ts b/src/config/default-settings.ts index bdcf049..37ce510 100644 --- a/src/config/default-settings.ts +++ b/src/config/default-settings.ts @@ -1,14 +1,10 @@ -import { runtimeControls } from './runtime-controls'; +import { INTERNAL_RENDER_AREA_MEGAPIXEL_LIMITS } from './runtime-setting-bounds'; import type { GardenAppConfig } from './types'; // Mirrors the historical render-scale cap so the default render area stays // roughly equivalent to native rendering on high-DPR phones without the // pipeline applying its own clamp. The slider can override freely. const DEFAULT_DEVICE_PIXEL_RATIO_CAP = 2; -const INTERNAL_RENDER_AREA_BOUNDS = { - min: runtimeControls.internalRenderAreaMegapixels?.min ?? 0.5, - max: runtimeControls.internalRenderAreaMegapixels?.max ?? 16.6, -}; const computeDefaultInternalRenderAreaMegapixels = (): number => { const rawDpr = @@ -20,8 +16,8 @@ const computeDefaultInternalRenderAreaMegapixels = (): number => { const cssHeight = typeof window !== 'undefined' ? window.innerHeight : 1080; const cssMegapixels = (Math.max(cssWidth, 1) * Math.max(cssHeight, 1)) / 1_000_000; return Math.min( - INTERNAL_RENDER_AREA_BOUNDS.max, - Math.max(INTERNAL_RENDER_AREA_BOUNDS.min, dpr * dpr * cssMegapixels) + INTERNAL_RENDER_AREA_MEGAPIXEL_LIMITS.max, + Math.max(INTERNAL_RENDER_AREA_MEGAPIXEL_LIMITS.min, dpr * dpr * cssMegapixels) ); }; diff --git a/src/config/runtime-controls.ts b/src/config/runtime-controls.ts index 82278b0..e07a259 100644 --- a/src/config/runtime-controls.ts +++ b/src/config/runtime-controls.ts @@ -1,4 +1,5 @@ import { colorInteractionControl } from './color-interactions'; +import { INTERNAL_RENDER_AREA_MEGAPIXEL_LIMITS } from './runtime-setting-bounds'; import type { GardenAppConfig } from './types'; const formatPercent = (value: number): string => `${Math.round(value * 100)}%`; @@ -55,6 +56,13 @@ export const runtimeControls: GardenAppConfig['runtimeSettings']['controls'] = { max: 200, step: 1, }, + sensorOffsetAngle: { + folder: 'Movement', + label: 'Sensor Angle', + min: 0, + max: 180, + step: 1, + }, moveSpeed: { folder: 'Movement', label: 'Travel Speed', @@ -81,7 +89,7 @@ export const runtimeControls: GardenAppConfig['runtimeSettings']['controls'] = { folder: 'Movement', label: 'Wander Turn', min: 0, - max: 6.28, + max: Math.PI * 2, step: 0.01, }, individualTrailWeight: { @@ -132,8 +140,8 @@ export const runtimeControls: GardenAppConfig['runtimeSettings']['controls'] = { internalRenderAreaMegapixels: { folder: 'Performance', label: 'Render Quality (MP)', - min: 0.5, - max: 16.6, + min: INTERNAL_RENDER_AREA_MEGAPIXEL_LIMITS.min, + max: INTERNAL_RENDER_AREA_MEGAPIXEL_LIMITS.max, step: 0.1, }, }; diff --git a/src/config/types.ts b/src/config/types.ts index 1c6bedf..456dec8 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -103,6 +103,12 @@ export interface VibePreset { export interface GardenAppConfig { audio: GardenAudioConfig; + analytics: { + autoCapturePageviews: boolean; + domain: string; + endpoint: string; + logging: boolean; + }; deltaTime: { maxDeltaTimeSeconds: number; minDeltaTimeSeconds: number; @@ -167,6 +173,7 @@ export interface GardenAppConfig { letterSpacingEm: number; maskAlphaThreshold: number; maskGradientThreshold: number; + maskMaxPixels: number; maskSampleDensity: number; maxHeightRatio: number; maxWidthRatio: number; diff --git a/src/config/vibe-presets.test.ts b/src/config/vibe-presets.test.ts index 608a86e..a69d524 100644 --- a/src/config/vibe-presets.test.ts +++ b/src/config/vibe-presets.test.ts @@ -1,13 +1,14 @@ import { describe, expect, it } from 'vitest'; +import { runtimeControls } from './runtime-controls'; import { vibePresets } from './vibe-presets'; const FINAL_VIBE_NAMES = [ 'Aurora Mycelium Copy', - 'Velvet Observatory', + 'Velvet Observatory Copy', 'Lichen Signal', 'Tidepool Lantern', - 'Paper Lantern Fog Copy', + 'Paper Lantern Fog', 'Chrome Pollen', ]; @@ -17,8 +18,8 @@ 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 MAX_SPAWN_PER_PIXEL = runtimeControls.spawnPerPixel?.max ?? 0.38; +const MAX_BRUSH_SIZE = runtimeControls.brushSize?.max ?? 36; const HIGH_DENSITY_SPAWN_THRESHOLD = 0.28; const HIGH_DENSITY_DECAY_LIMIT = 940; const HIGH_DENSITY_BRUSH_SIZE_LIMIT = 14; diff --git a/src/config/vibe-presets.ts b/src/config/vibe-presets.ts index c5c7d40..7a30359 100644 --- a/src/config/vibe-presets.ts +++ b/src/config/vibe-presets.ts @@ -139,7 +139,7 @@ export const vibePresets: Array = [ id: VibeId.AuroraMycelium, name: 'Aurora Mycelium Copy', colors: [ - [221, 255, 78], + [251, 210, 94], [154, 99, 255], [255, 31, 199], ], @@ -148,25 +148,25 @@ export const vibePresets: Array = [ ...colorReactions.auroraMycelium, backgroundGrainStrength: 0.003, brushSize: 8.75, - clarity: 0.379, - decayRateTrails: 940, - forwardRotationScale: 0, - individualTrailWeight: 0.121, - moveSpeed: 270, - sensorOffsetAngle: 36, - sensorOffsetDistance: 51, - spawnPerPixel: 0.14, + clarity: 1, + decayRateTrails: 973, + forwardRotationScale: 0.37, + individualTrailWeight: 0.053000000000000005, + moveSpeed: 144, + sensorOffsetAngle: 35, + sensorOffsetDistance: 52, + spawnPerPixel: 0.13999999999999999, strokeAngleJitterRadians: 0.45, - turnSpeed: 22, + turnSpeed: 13, turnWhenLost: 0, }, audio: { ...defaultGardenAudioVibeSettings, - idleIntensity: 0.12, + idleIntensity: 0.12000000000000002, bpm: 60, rampUpIntensity: 0.7, rampUpTime: 0.14, - noteLength: 0.86, + noteLength: 0.8599999999999999, notePitchOffset: -2, brightness: 0.84, scale: musicScales.lydian, @@ -175,7 +175,7 @@ export const vibePresets: Array = [ }, { id: VibeId.VelvetObservatory, - name: 'Velvet Observatory', + name: 'Velvet Observatory Copy', colors: [ [178, 76, 62], [2, 174, 255], @@ -186,21 +186,21 @@ export const vibePresets: Array = [ ...colorReactions.velvetObservatory, backgroundGrainStrength: 0.005, brushSize: 9.75, - clarity: 0.437, - decayRateTrails: 915, + clarity: 1, + decayRateTrails: 974, forwardRotationScale: 0, - individualTrailWeight: 0.1, - moveSpeed: 216, + individualTrailWeight: 0.232, + moveSpeed: 121, sensorOffsetAngle: 24, sensorOffsetDistance: 17, - spawnPerPixel: 0.24, + spawnPerPixel: 0.11499999999999999, strokeAngleJitterRadians: 0.17, turnSpeed: 33, turnWhenLost: 0.42, }, audio: { ...defaultGardenAudioVibeSettings, - idleIntensity: 0.55, + idleIntensity: 0.24000000000000002, bpm: 72, rampUpIntensity: 1.42, rampUpTime: 0.07, @@ -289,7 +289,7 @@ export const vibePresets: Array = [ }, { id: VibeId.PaperLanternFog, - name: 'Paper Lantern Fog Copy', + name: 'Paper Lantern Fog', colors: [ [255, 176, 108], [239, 90, 108], diff --git a/src/game-loop/agent-population.test.ts b/src/game-loop/agent-population.test.ts index f49a3ab..2bc0e26 100644 --- a/src/game-loop/agent-population.test.ts +++ b/src/game-loop/agent-population.test.ts @@ -145,6 +145,18 @@ describe('AgentPopulation stroke spawning', () => { expect(pipeline.writtenBatches[0][2]).toBe(0); }); + it('clears active agents when an intro replacement has no generated agents', () => { + const { population } = createPopulation(); + + population.spawnStrokeAgents(vec2.fromValues(0, 0), vec2.fromValues(3, 0)); + expect(population.activeAgentCount).toBe(3); + + settings.maxAgentCount = 0; + population.replaceIntroAgents(vec2.fromValues(100, 100), 0); + + expect(population.activeAgentCount).toBe(0); + }); + it('queues stroke writes while async compaction is in flight', async () => { const { pipeline, population } = createPopulation(); diff --git a/src/game-loop/agent-population.ts b/src/game-loop/agent-population.ts index 68e05c0..1d0390f 100644 --- a/src/game-loop/agent-population.ts +++ b/src/game-loop/agent-population.ts @@ -59,6 +59,8 @@ export class AgentPopulation { }); if (data.length === 0) { + this.activeCount = 0; + this.replacementCursor = 0; return; } diff --git a/src/game-loop/frame-performance.ts b/src/game-loop/frame-performance.ts index 5cab70d..9fc249e 100644 --- a/src/game-loop/frame-performance.ts +++ b/src/game-loop/frame-performance.ts @@ -46,14 +46,16 @@ export class FramePerformance { return; } - const fps = 1 / deltaSeconds; - this.frameDeltaSeconds = deltaSeconds; this.measuredFrameTimeMs = deltaSeconds * 1000; + const fps = 1 / deltaSeconds; this.measuredFps = fps; if (deltaSeconds > FRAME_GAP_RESET_SECONDS) { + this.frameDeltaSeconds = 0; + this.smoothedFps = ADAPTIVE_REFRESH_TARGET_FPS; return; } + this.frameDeltaSeconds = deltaSeconds; this.smoothedFps = this.smoothedFps * FPS_SMOOTHING_RETAIN + fps * FPS_SMOOTHING_NEW; } } diff --git a/src/game-loop/game-loop-resources.ts b/src/game-loop/game-loop-resources.ts index 14588f9..64c9680 100644 --- a/src/game-loop/game-loop-resources.ts +++ b/src/game-loop/game-loop-resources.ts @@ -1,6 +1,6 @@ import { vec2 } from 'gl-matrix'; -import { appConfig } from '../config'; +import { appConfig, type GardenRuntimeSettings } from '../config'; import { AgentGenerationPipeline } from '../pipelines/agents/agent-generation/agent-generation-pipeline'; import { AgentPipeline } from '../pipelines/agents/agent-pipeline'; import { BrushPipeline } from '../pipelines/brush/brush-pipeline'; @@ -9,7 +9,6 @@ import { DiffusionPipeline } from '../pipelines/diffusion/diffusion-pipeline'; import { EraserAgentPipeline } from '../pipelines/eraser/eraser-agent-pipeline'; import { EraserTexturePipeline } from '../pipelines/eraser/eraser-texture-pipeline'; import { RenderPipeline } from '../pipelines/render/render-pipeline'; -import { settings } from '../settings'; import { initializeContext } from '../utils/graphics/initialize-context'; import { CanvasReadbackRequest, RenderInputs } from './game-loop-types'; import { GpuProfiler } from './gpu-profiler'; @@ -25,6 +24,7 @@ interface FrameParameters extends RenderInputs { introProgress: number; selectedColorIndex: number; eraserPixelSize: number; + runtimeSettings: GardenRuntimeSettings; } export class GameLoopResources { @@ -46,7 +46,8 @@ export class GameLoopResources { private readonly device: GPUDevice, private readonly canvasFormat: GPUTextureFormat, canvasSize: vec2, - initialAgentCapacity: number + initialAgentCapacity: number, + initialMaxAgentCount: number ) { const context = initializeContext({ device, canvas, format: canvasFormat }); @@ -59,7 +60,7 @@ export class GameLoopResources { this.agentGenerationPipeline = new AgentGenerationPipeline( this.device, - Math.min(settings.maxAgentCount, initialAgentCapacity) + Math.min(initialMaxAgentCount, initialAgentCapacity) ); this.agentPipeline = new AgentPipeline( @@ -74,12 +75,7 @@ export class GameLoopResources { ); this.eraserTexturePipeline = new EraserTexturePipeline(this.device, this.commonState); this.diffusionPipeline = new DiffusionPipeline(this.device); - this.renderPipeline = new RenderPipeline( - context, - this.device, - this.commonState, - this.canvasFormat - ); + this.renderPipeline = new RenderPipeline(context, this.device, this.canvasFormat); this.gpuProfiler = GpuProfiler.create( this.device, () => appConfig.tuningPane.showFpsOverlay @@ -128,43 +124,43 @@ export class GameLoopResources { channelColors, backgroundColor, eraserPixelSize, + runtimeSettings, }: FrameParameters): void { this.commonState.setParameters({ canvasSize, }); this.agentPipeline.setParameters({ - ...settings, + ...runtimeSettings, deltaTime, time, agentCount: activeAgentCount, - moveSpeed: settings.moveSpeed, introMoveSpeed: appConfig.simulation.introMoveSpeed, introProgress, }); this.brushPipeline.setParameters({ - ...settings, + ...runtimeSettings, pixelRatio: canvasPixelRatio, selectedColorIndex, }); - this.diffusionPipeline.setParameters(settings); + this.diffusionPipeline.setParameters(runtimeSettings); this.renderPipeline.setParameters({ - ...settings, + ...runtimeSettings, channelColors, backgroundColor, }); this.eraserAgentPipeline.setParameters({ agentCount: activeAgentCount, eraserSize: eraserPixelSize, - eraserMaskAlphaThreshold: settings.eraserMaskAlphaThreshold, + eraserMaskAlphaThreshold: runtimeSettings.eraserMaskAlphaThreshold, maskSize: canvasSize, }); this.eraserTexturePipeline.setParameters({ eraserSize: eraserPixelSize, - eraserLineDistanceEpsilon: settings.eraserLineDistanceEpsilon, - eraserClearRed: settings.eraserClearRed, - eraserClearGreen: settings.eraserClearGreen, - eraserClearBlue: settings.eraserClearBlue, - eraserClearAlpha: settings.eraserClearAlpha, + eraserLineDistanceEpsilon: runtimeSettings.eraserLineDistanceEpsilon, + eraserClearRed: runtimeSettings.eraserClearRed, + eraserClearGreen: runtimeSettings.eraserClearGreen, + eraserClearBlue: runtimeSettings.eraserClearBlue, + eraserClearAlpha: runtimeSettings.eraserClearAlpha, }); } diff --git a/src/game-loop/game-loop.ts b/src/game-loop/game-loop.ts index 6cabd79..aed5289 100644 --- a/src/game-loop/game-loop.ts +++ b/src/game-loop/game-loop.ts @@ -31,7 +31,6 @@ export default class GameLoop { private readonly toolbarContrastMonitor: ToolbarContrastMonitor; private readonly seedValue = Math.floor(Math.random() * 0xffffffff); private readonly seed = this.seedValue.toString(16); - private readonly resizeListener = this.resize.bind(this); private readonly _canvasSize: vec2 = vec2.create(); private pendingIntroResizeAt: DOMHighResTimeStamp | null = null; @@ -55,7 +54,8 @@ export default class GameLoop { device, this.canvasFormat, this.canvasSize, - this.framePerformance.adaptiveCapInitial + this.framePerformance.adaptiveCapInitial, + settings.maxAgentCount ); this.introPrompt = new IntroPrompt(ui.prompt); this.toolbarContrastMonitor = new ToolbarContrastMonitor( @@ -112,13 +112,12 @@ export default class GameLoop { getVibeId: () => activeVibe.id, }); - window.addEventListener('resize', this.resizeListener); - this.eraserPreview.attach(); this.syncPerfStatsOverlay(); } public attachPointerInput(): void { this.pointerInput.attach(); + this.eraserPreview.attach(); } public setEraseMode(isErasing: boolean): void { @@ -173,9 +172,6 @@ export default class GameLoop { cancelAnimationFrame(this.animationFrameId); this.animationFrameId = null; } - this.finished.resolve(); - - window.removeEventListener('resize', this.resizeListener); this.pointerInput.detach(); this.eraserPreview.detach(); this.perfStatsOverlay?.destroy(); @@ -185,6 +181,7 @@ export default class GameLoop { await this.agentPopulation.waitForCompaction(); this.resources.destroy(); await this.audio.destroy(); + this.finished.resolve(); } private readonly render = (time: DOMHighResTimeStamp) => { @@ -204,13 +201,15 @@ export default class GameLoop { const channelColors = activeVibe.colors; const backgroundColor = activeVibe.backgroundColor; + const runtimeSettings = { ...settings }; const introProgress = this.introPrompt.progress; const canvasPixelRatio = this.canvasPixelRatio; - const eraserPixelSize = settings.eraserSize * canvasPixelRatio; + const eraserPixelSize = runtimeSettings.eraserSize * canvasPixelRatio; const isErasing = this.pointerInput.isEraseMode; - const accentColor = channelColors[settings.selectedColorIndex] ?? channelColors[0]; + const accentColor = + channelColors[runtimeSettings.selectedColorIndex] ?? channelColors[0]; this.updateAccentColor(accentColor); - this.updateGrainOverlay(settings.backgroundGrainStrength); + this.updateGrainOverlay(runtimeSettings.backgroundGrainStrength); this.audio.update({ vibe: activeVibe, isErasing, @@ -223,10 +222,11 @@ export default class GameLoop { activeAgentCount: this.agentPopulation.activeAgentCount, canvasPixelRatio, introProgress, - selectedColorIndex: settings.selectedColorIndex, + selectedColorIndex: runtimeSettings.selectedColorIndex, channelColors, backgroundColor, eraserPixelSize, + runtimeSettings, }); this.resources.executeFrame( diff --git a/src/game-loop/intro-title-agents.ts b/src/game-loop/intro-title-agents.ts index 2d530cb..a34bbf4 100644 --- a/src/game-loop/intro-title-agents.ts +++ b/src/game-loop/intro-title-agents.ts @@ -1,4 +1,4 @@ -import { appConfig } from '../config'; +import { appConfig, type GardenAppConfig } from '../config'; import { AGENT_FLOAT_COUNT, writeAgentValues } from '../pipelines/agents/agent-limits'; import { clamp, easeOutQuad, mix, mixAngle, smoothstep } from '../utils/math'; @@ -18,8 +18,11 @@ interface IntroTitleAgentOptions { } type RandomSource = () => number; +type IntroPathEasing = GardenAppConfig['simulation']['intro']['pathEasing']; const INTRO_TITLE = appConfig.simulation.intro.title; +const isLinearPathEasing = (pathEasing: IntroPathEasing): boolean => + pathEasing === 'linear'; export const createIntroTitleAgents = ({ count, @@ -169,16 +172,22 @@ const createIntroTitlePoints = ( width: number, height: number ): Array => { + const safeMaxPixels = Math.max(1, appConfig.simulation.intro.maskMaxPixels); + const maskScale = Math.min(1, Math.sqrt(safeMaxPixels / Math.max(1, width * height))); + const maskWidth = Math.max(1, Math.round(width * maskScale)); + const maskHeight = Math.max(1, Math.round(height * maskScale)); + const pointScaleX = width / maskWidth; + const pointScaleY = height / maskHeight; const maskCanvas = document.createElement('canvas'); - maskCanvas.width = width; - maskCanvas.height = height; + maskCanvas.width = maskWidth; + maskCanvas.height = maskHeight; const context = maskCanvas.getContext('2d', { willReadFrequently: true }); if (!context) { return []; } - const fontSize = getIntroTitleFontSize(context, width, height); - context.clearRect(0, 0, width, height); + const fontSize = getIntroTitleFontSize(context, maskWidth, maskHeight); + context.clearRect(0, 0, maskWidth, maskHeight); context.font = `${fontSize}px ${appConfig.simulation.intro.fontFamily}`; context.textAlign = 'center'; context.textBaseline = 'middle'; @@ -192,42 +201,44 @@ const createIntroTitlePoints = ( const letterSpacing = fontSize * appConfig.simulation.intro.letterSpacingEm; drawIntroTitleText( context, - width / 2, - height * appConfig.simulation.intro.verticalAnchor, + maskWidth / 2, + maskHeight * appConfig.simulation.intro.verticalAnchor, letterSpacing, 'stroke' ); drawIntroTitleText( context, - width / 2, - height * appConfig.simulation.intro.verticalAnchor, + maskWidth / 2, + maskHeight * appConfig.simulation.intro.verticalAnchor, letterSpacing, 'fill' ); - const { data } = context.getImageData(0, 0, width, height); + const { data } = context.getImageData(0, 0, maskWidth, maskHeight); const step = Math.max( 1, - Math.floor(Math.min(width, height) / appConfig.simulation.intro.maskSampleDensity) + Math.floor( + Math.min(maskWidth, maskHeight) / appConfig.simulation.intro.maskSampleDensity + ) ); const points: Array = []; const characterColorBoundaries = getIntroTitleColorBoundaries( context, - width, + maskWidth, letterSpacing ); - for (let y = 0; y < height; y += step) { - for (let x = 0; x < width; x += step) { - const alpha = getMaskAlpha(data, width, height, x, y); + for (let y = 0; y < maskHeight; y += step) { + for (let x = 0; x < maskWidth; x += step) { + const alpha = getMaskAlpha(data, maskWidth, maskHeight, x, y); if (alpha < appConfig.simulation.intro.maskAlphaThreshold) { continue; } points.push({ - x, - y, - tangent: estimateMaskTangent(data, width, height, x, y), + x: x * pointScaleX, + y: y * pointScaleY, + tangent: estimateMaskTangent(data, maskWidth, maskHeight, x, y), colorIndex: getIntroTitleColorIndex(x, characterColorBoundaries), }); } @@ -244,8 +255,10 @@ const getIntroTitleColorBoundaries = ( const letters = Array.from(INTRO_TITLE); const totalWidth = measureIntroTitleText(context, letters, letterSpacing); let x = width / 2 - totalWidth / 2; - const [firstCutLetter, secondCutLetter] = - appConfig.simulation.intro.titleColorCutLetters; + const cutLetters = appConfig.simulation.intro.titleColorCutLetters + .map((cutLetter) => Math.min(letters.length - 1, Math.max(1, Math.round(cutLetter)))) + .sort((a, b) => a - b); + const [firstCutLetter, secondCutLetter] = cutLetters; const letterBoxes = letters.map((letter, index) => { const letterWidth = context.measureText(letter).width; const box = { @@ -401,7 +414,7 @@ const createSeededRandom = (seed: number): RandomSource => { }; const easePathProgress = (amount: number): number => { - if (appConfig.simulation.intro.pathEasing === 'linear') { + if (isLinearPathEasing(appConfig.simulation.intro.pathEasing)) { return amount; } diff --git a/src/game-loop/perf-stats-overlay.ts b/src/game-loop/perf-stats-overlay.ts index 6ffd55b..9e6717f 100644 --- a/src/game-loop/perf-stats-overlay.ts +++ b/src/game-loop/perf-stats-overlay.ts @@ -1,4 +1,5 @@ const PERF_STATS_REFRESH_MS = 200; +const UNAVAILABLE_STAT_TEXT = 'n/a'; const ZERO_STAT_TEXT = '0'; const ZERO_FRAME_TIME_TEXT = '0ms'; const ZERO_RESOLUTION_TEXT = '0x0'; @@ -39,7 +40,7 @@ export class PerfStatsOverlay { } this.previousUpdateTime = time; - const text = `FPS ${formatFps(fps)}\nAgents ${formatAgentCount(agentCount)}\nFrame ${formatFrameTime(frameTimeMs)}\nGPU passes ${formatFrameTime(gpuPassTimeMs)}\nResolution ${formatResolution(renderWidth, renderHeight)}`; + const text = `FPS ${formatFps(fps)}\nAgents ${formatAgentCount(agentCount)}\nFrame ${formatFrameTime(frameTimeMs)}\nGPU passes ${formatOptionalFrameTime(gpuPassTimeMs)}\nResolution ${formatResolution(renderWidth, renderHeight)}`; if (text !== this.previousText) { this.element.textContent = text; this.previousText = text; @@ -68,6 +69,11 @@ const formatFrameTime = (frameTimeMs: number | undefined): string => { return `${safeFrameTimeMs.toFixed(safeFrameTimeMs < 10 ? 1 : 0)}ms`; }; +const formatOptionalFrameTime = (frameTimeMs: number | undefined): string => + typeof frameTimeMs === 'number' && Number.isFinite(frameTimeMs) + ? formatFrameTime(frameTimeMs) + : UNAVAILABLE_STAT_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))}` diff --git a/src/game-loop/pointer-input.ts b/src/game-loop/pointer-input.ts index ae5fed0..bee83ef 100644 --- a/src/game-loop/pointer-input.ts +++ b/src/game-loop/pointer-input.ts @@ -96,7 +96,6 @@ export class GardenPointerInput { this.options.onStartDrawing(); this.activePointerId = event.pointerId; this.canvas.setPointerCapture(event.pointerId); - this.options.strokeOutput.clearSwipes(); this.lastPointerPosition = null; this.lastPointerEventTimeMs = null; this.brushSmoother.clear(); @@ -122,11 +121,16 @@ export class GardenPointerInput { if (this.isErasing) { this.options.onEraseGestureEnded(); } - this.canvas.releasePointerCapture(event.pointerId); - this.activePointerId = null; - this.lastPointerPosition = null; - this.lastPointerEventTimeMs = null; - this.brushSmoother.clear(); + try { + if (this.canvas.hasPointerCapture(event.pointerId)) { + this.canvas.releasePointerCapture(event.pointerId); + } + } finally { + this.activePointerId = null; + this.lastPointerPosition = null; + this.lastPointerEventTimeMs = null; + this.brushSmoother.clear(); + } }; private addSwipeAt(event: PointerEvent, options: { emitAudio?: boolean } = {}): void { diff --git a/src/game-loop/simulation-frame.ts b/src/game-loop/simulation-frame.ts index ec4b9e3..36629e8 100644 --- a/src/game-loop/simulation-frame.ts +++ b/src/game-loop/simulation-frame.ts @@ -68,7 +68,7 @@ export class SimulationFrameRenderer { ); } } else { - wroteSourceMap = this.pipelines.brushPipeline.executeMultiTarget( + wroteSourceMap = this.pipelines.brushPipeline.executeSource( commandEncoder, this.textures.sourceMapA.getTextureView(), this.gpuProfiler?.timestampWrites('brush') diff --git a/src/game-loop/simulation-textures.ts b/src/game-loop/simulation-textures.ts index d2256d0..7166595 100644 --- a/src/game-loop/simulation-textures.ts +++ b/src/game-loop/simulation-textures.ts @@ -13,7 +13,7 @@ export class SimulationTextures { // diffused texture becomes trailMapA for the next frame. public trailMapA: ResizableTexture; public trailMapB: ResizableTexture; - // Per-frame deposit accumulator: cleared each frame, written sparsely by + // Per-frame last-writer deposit map: cleared each frame, written sparsely by // agents, then read by diffuse alongside trailMapA. public readonly depositMap: ResizableTexture; public readonly eraserMask: ResizableTexture; diff --git a/src/game-loop/toolbar-contrast-monitor.ts b/src/game-loop/toolbar-contrast-monitor.ts index 9c908fe..8898123 100644 --- a/src/game-loop/toolbar-contrast-monitor.ts +++ b/src/game-loop/toolbar-contrast-monitor.ts @@ -7,6 +7,14 @@ interface CanvasSamplePoint { y: number; } +interface CanvasSampleRegion { + bytesPerRow: number; + height: number; + origin: CanvasSamplePoint; + sampleOffsets: Array; + width: number; +} + interface ToolbarContrastMetrics { averageLuminance: number; backgroundOpacity: number; @@ -16,6 +24,7 @@ interface ToolbarContrastMetrics { const TOOLBAR_BACKGROUND_OPACITY_PROPERTY = '--toolbar-background-opacity'; const TOOLBAR_BACKGROUND_STRENGTH_PROPERTY = '--toolbar-background-strength'; +const GPU_COPY_BYTES_PER_ROW_ALIGNMENT = 256; const getLinearChannel = (channel: number): number => { const normalized = channel / 255; @@ -33,16 +42,13 @@ const getRelativeLuminance = (red: number, green: number, blue: number): number const getToolbarContrastMetrics = ( pixels: Uint8Array, - sampleCount: number, + sampleOffsets: ReadonlyArray, isBgra: boolean ): ToolbarContrastMetrics => { - const count = Math.max( - 0, - Math.min( - sampleCount, - Math.floor(pixels.length / appConfig.toolbar.contrast.bytesPerSample) - ) - ); + const count = sampleOffsets.filter( + (offset) => + offset >= 0 && offset + appConfig.toolbar.contrast.bytesPerSample <= pixels.length + ).length; if (count === 0) { return { averageLuminance: 0, @@ -56,8 +62,14 @@ const getToolbarContrastMetrics = ( let brightCount = 0; let lowContrastCount = 0; - for (let i = 0; i < count; i++) { - const offset = i * appConfig.toolbar.contrast.bytesPerSample; + sampleOffsets.forEach((offset) => { + if ( + offset < 0 || + offset + appConfig.toolbar.contrast.bytesPerSample > pixels.length + ) { + return; + } + const red = pixels[offset + (isBgra ? 2 : 0)]; const green = pixels[offset + 1]; const blue = pixels[offset + (isBgra ? 0 : 2)]; @@ -73,7 +85,7 @@ const getToolbarContrastMetrics = ( if (contrastWithWhite < appConfig.toolbar.contrast.lowContrastThreshold) { lowContrastCount++; } - } + }); const averageLuminance = luminanceTotal / count; const brightRatio = brightCount / count; @@ -100,6 +112,8 @@ export class ToolbarContrastMonitor { private isDestroyed = false; private isReadbackPending = false; private lastSampleAt = Number.NEGATIVE_INFINITY; + private readbackBuffer: GPUBuffer | null = null; + private readbackBufferSize = 0; public constructor( private readonly canvas: HTMLCanvasElement, @@ -119,45 +133,29 @@ export class ToolbarContrastMonitor { return null; } - const samplePoints = this.getSamplePoints(); - if (samplePoints.length === 0) { + const sampleRegion = this.getSampleRegion(); + if (sampleRegion.sampleOffsets.length === 0) { return null; } - let buffer: GPUBuffer; - try { - buffer = this.device.createBuffer({ - size: samplePoints.length * appConfig.toolbar.contrast.bytesPerSample, - usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, - }); - } catch { + const bufferSize = sampleRegion.bytesPerRow * sampleRegion.height; + const buffer = this.getReadbackBuffer(bufferSize); + if (!buffer) { return null; } this.isReadbackPending = true; this.lastSampleAt = time; - let isBufferDestroyed = false; let isCancelled = false; let isEncoded = false; - const destroyBuffer = () => { - if (isBufferDestroyed) { - return; - } - - isBufferDestroyed = true; - buffer.destroy(); - }; - const cancel = (destroyNow = true) => { + const cancel = () => { if (isCancelled) { return; } isCancelled = true; this.isReadbackPending = false; - if (destroyNow) { - destroyBuffer(); - } }; return { @@ -167,31 +165,28 @@ export class ToolbarContrastMonitor { } try { - samplePoints.forEach((point, index) => { - commandEncoder.copyTextureToBuffer( - { - origin: point, - texture, - }, - { - buffer, - offset: index * appConfig.toolbar.contrast.bytesPerSample, - }, - { - depthOrArrayLayers: 1, - height: 1, - width: 1, - } - ); - }); + commandEncoder.copyTextureToBuffer( + { + origin: sampleRegion.origin, + texture, + }, + { + buffer, + bytesPerRow: sampleRegion.bytesPerRow, + }, + { + depthOrArrayLayers: 1, + height: sampleRegion.height, + width: sampleRegion.width, + } + ); isEncoded = true; } catch { - cancel(false); + cancel(); } }, afterSubmit: () => { if (isCancelled) { - destroyBuffer(); return; } @@ -200,13 +195,16 @@ export class ToolbarContrastMonitor { return; } - void this.readBuffer(buffer, samplePoints.length); + void this.readBuffer(buffer, sampleRegion.sampleOffsets); }, }; } public destroy(): void { this.isDestroyed = true; + this.readbackBuffer?.destroy(); + this.readbackBuffer = null; + this.readbackBufferSize = 0; this.toolbar.style.removeProperty(TOOLBAR_BACKGROUND_OPACITY_PROPERTY); this.toolbar.style.removeProperty(TOOLBAR_BACKGROUND_STRENGTH_PROPERTY); } @@ -231,7 +229,14 @@ export class ToolbarContrastMonitor { ); } - private getSamplePoints(): Array { + private getSampleRegion(): CanvasSampleRegion { + const emptyRegion = { + bytesPerRow: 0, + height: 0, + origin: { x: 0, y: 0 }, + sampleOffsets: [], + width: 0, + }; const canvasRect = this.canvas.getBoundingClientRect(); const toolbarRect = this.toolbar.getBoundingClientRect(); if ( @@ -240,7 +245,7 @@ export class ToolbarContrastMonitor { toolbarRect.width <= 0 || toolbarRect.height <= 0 ) { - return []; + return emptyRegion; } const left = Math.max(canvasRect.left, toolbarRect.left); @@ -248,17 +253,40 @@ export class ToolbarContrastMonitor { const top = Math.max(canvasRect.top, toolbarRect.top); const bottom = Math.min(canvasRect.bottom, toolbarRect.bottom); if (left >= right || top >= bottom) { - return []; + return emptyRegion; } const xScale = this.canvas.width / canvasRect.width; const yScale = this.canvas.height / canvasRect.height; - const width = right - left; - const height = bottom - top; + const cssWidth = right - left; + const cssHeight = bottom - top; + const origin = { + x: Math.max(0, Math.floor((left - canvasRect.left) * xScale)), + y: Math.max(0, Math.floor((top - canvasRect.top) * yScale)), + }; + const regionRight = Math.min( + this.canvas.width, + Math.ceil((right - canvasRect.left) * xScale) + ); + const regionBottom = Math.min( + this.canvas.height, + Math.ceil((bottom - canvasRect.top) * yScale) + ); + const width = Math.max(0, regionRight - origin.x); + const height = Math.max(0, regionBottom - origin.y); + if (width === 0 || height === 0) { + return emptyRegion; + } + + const bytesPerRow = alignTo( + width * appConfig.toolbar.contrast.bytesPerSample, + GPU_COPY_BYTES_PER_ROW_ALIGNMENT + ); const points = new Map(); for (let row = 0; row < appConfig.toolbar.contrast.sampleRows; row++) { - const cssY = top + ((row + 0.5) / appConfig.toolbar.contrast.sampleRows) * height; + const cssY = + top + ((row + 0.5) / appConfig.toolbar.contrast.sampleRows) * cssHeight; const y = Math.min( this.canvas.height - 1, Math.max(0, Math.floor((cssY - canvasRect.top) * yScale)) @@ -266,7 +294,7 @@ export class ToolbarContrastMonitor { for (let column = 0; column < appConfig.toolbar.contrast.sampleColumns; column++) { const cssX = - left + ((column + 0.5) / appConfig.toolbar.contrast.sampleColumns) * width; + left + ((column + 0.5) / appConfig.toolbar.contrast.sampleColumns) * cssWidth; const x = Math.min( this.canvas.width - 1, Math.max(0, Math.floor((cssX - canvasRect.left) * xScale)) @@ -275,10 +303,43 @@ export class ToolbarContrastMonitor { } } - return [...points.values()]; + return { + bytesPerRow, + height, + origin, + sampleOffsets: [...points.values()].map( + (point) => + (point.y - origin.y) * bytesPerRow + + (point.x - origin.x) * appConfig.toolbar.contrast.bytesPerSample + ), + width, + }; } - private async readBuffer(buffer: GPUBuffer, sampleCount: number): Promise { + private getReadbackBuffer(size: number): GPUBuffer | null { + if (this.readbackBuffer && this.readbackBufferSize >= size) { + return this.readbackBuffer; + } + + this.readbackBuffer?.destroy(); + try { + this.readbackBuffer = this.device.createBuffer({ + size, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ, + }); + this.readbackBufferSize = size; + return this.readbackBuffer; + } catch { + this.readbackBuffer = null; + this.readbackBufferSize = 0; + return null; + } + } + + private async readBuffer( + buffer: GPUBuffer, + sampleOffsets: Array + ): Promise { let isMapped = false; try { await buffer.mapAsync(GPUMapMode.READ); @@ -286,7 +347,7 @@ export class ToolbarContrastMonitor { if (!this.isDestroyed) { const pixels = new Uint8Array(buffer.getMappedRange()); - const metrics = getToolbarContrastMetrics(pixels, sampleCount, this.isBgra); + const metrics = getToolbarContrastMetrics(pixels, sampleOffsets, this.isBgra); this.setToolbarBackgroundOpacity(metrics.backgroundOpacity); } } catch { @@ -295,8 +356,10 @@ export class ToolbarContrastMonitor { if (isMapped) { buffer.unmap(); } - buffer.destroy(); this.isReadbackPending = false; } } } + +const alignTo = (value: number, alignment: number): number => + Math.ceil(value / alignment) * alignment; diff --git a/src/index.ts b/src/index.ts index db0425f..74b98d7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -82,11 +82,13 @@ const main = async () => { ); const splash = new SplashScreen(); + let eraserSizeControl: EraserSizeControl | null = null; const paletteControl = new PaletteControl({ getGame, onChange: () => configPane?.refresh(), + onModeChange: (isEraserActive) => eraserSizeControl?.setActive(isEraserActive), }); - const eraserSizeControl = new EraserSizeControl({ + eraserSizeControl = new EraserSizeControl({ getGame, onActivate: () => paletteControl.setEraserActive(true), onChange: () => configPane?.refresh(), @@ -104,7 +106,8 @@ const main = async () => { }); const syncRuntimeUi = () => { - eraserSizeControl.render(); + eraserSizeControl?.render(); + eraserSizeControl?.setActive(paletteControl.isEraserActive); mirrorSegmentControl.render(); paletteControl.render(); }; @@ -243,7 +246,6 @@ const main = async () => { ErrorPresenter.renderStartup(e); ErrorHandler.addException(e); } - console.error(e); } }; diff --git a/src/page/collapsible-panel-animator.ts b/src/page/collapsible-panel-animator.ts index 4b10e52..a86a637 100644 --- a/src/page/collapsible-panel-animator.ts +++ b/src/page/collapsible-panel-animator.ts @@ -1,6 +1,7 @@ export class CollapsiblePanelAnimator { private _isOpen = false; private focusBeforeOpen: HTMLElement | null = null; + private readonly abortController = new AbortController(); public onOpen?: () => void; public constructor( @@ -8,17 +9,23 @@ export class CollapsiblePanelAnimator { private readonly collapsibleContent: HTMLElement, ignoreForCloseOnClick: HTMLElement ) { - toggleButton.addEventListener('click', this.toggle.bind(this)); + const { signal } = this.abortController; + toggleButton.addEventListener('click', this.toggle, { signal }); window.addEventListener( 'click', - (event) => !ignoreForCloseOnClick.contains(event.target as Node) && this.close() + (event) => !ignoreForCloseOnClick.contains(event.target as Node) && this.close(), + { signal } + ); + window.addEventListener( + 'keydown', + (event) => { + if (this._isOpen && event.key === 'Escape') { + event.preventDefault(); + this.close(); + } + }, + { signal } ); - window.addEventListener('keydown', (event) => { - if (this._isOpen && event.key === 'Escape') { - event.preventDefault(); - this.close(); - } - }); this.syncAccessibility(); } @@ -49,12 +56,16 @@ export class CollapsiblePanelAnimator { } } - public toggle() { + public readonly toggle = () => { if (this._isOpen) { this.close(); } else { this.open(); } + }; + + public destroy(): void { + this.abortController.abort(); } public get isOpen() { diff --git a/src/page/config-pane.ts b/src/page/config-pane.ts index 030f631..3e6c397 100644 --- a/src/page/config-pane.ts +++ b/src/page/config-pane.ts @@ -9,15 +9,10 @@ import { type NumberControlConfig, } from '../config'; import { activeVibe, settings } from '../settings'; -import { - hexColorToRgbColor, - rgbColorToCss, - rgbColorToHex, - type RgbColor, -} from '../utils/rgb-color'; +import { hexColorToRgbColor, rgbColorToHex, type RgbColor } from '../utils/rgb-color'; +import { ColorReactionMatrixControl } from './color-reaction-matrix-control'; type PaneContainer = Pick; -type ColorReactionKey = (typeof colorReactionRows)[number]['keys'][number]; type RuntimeControlKey = keyof GardenRuntimeSettings & string; type VibeColorKey = 'color1' | 'color2' | 'color3' | 'backgroundColor'; type NumberPropertyKey = { @@ -33,31 +28,6 @@ interface PaneState extends GardenAudioVibeSettings { color3: string; } -const COLOR_REACTION_LABELS = ['Color 1', 'Color 2', 'Color 3'] as const; -const COLOR_REACTION_STATES = [ - { id: 'follow', label: 'Move Toward', value: 1 }, - { id: 'ignore', label: 'Ignore', value: 0 }, - { id: 'avoid', label: 'Move Away', value: -1 }, -] as const; - -const colorReactionRows = [ - { - colorIndex: 0, - label: COLOR_REACTION_LABELS[0], - keys: ['color1ToColor1', 'color1ToColor2', 'color1ToColor3'], - }, - { - colorIndex: 1, - label: COLOR_REACTION_LABELS[1], - keys: ['color2ToColor1', 'color2ToColor2', 'color2ToColor3'], - }, - { - colorIndex: 2, - label: COLOR_REACTION_LABELS[2], - keys: ['color3ToColor1', 'color3ToColor2', 'color3ToColor3'], - }, -] as const; - const runtimeFolderOrder = ['Brush', 'Movement', 'Look', 'Performance'] as const; const MUSIC_CONTROLS: ReadonlyArray<{ @@ -111,37 +81,11 @@ const getNumberBindingParams = (config: NumberControlConfig): BindingParams => { return params; }; -const getColorReactionStateIndex = (value: number): number => - COLOR_REACTION_STATES.findIndex((state) => state.value === value); - -const getColorReactionState = (value: number): (typeof COLOR_REACTION_STATES)[number] => - COLOR_REACTION_STATES[getColorReactionStateIndex(value)] ?? COLOR_REACTION_STATES[1]; - -const getNextColorReactionState = ( - value: number -): (typeof COLOR_REACTION_STATES)[number] => { - const index = getColorReactionStateIndex(value); - return COLOR_REACTION_STATES[ - ((index < 0 ? 1 : index) + 1) % COLOR_REACTION_STATES.length - ]; -}; - export class ConfigPane { private readonly container: HTMLDivElement; private readonly closeButton: HTMLButtonElement; + private readonly colorReactionMatrix: ColorReactionMatrixControl; private readonly pane: Pane; - private readonly colorReactionButtons = new Map< - ColorReactionKey, - { - element: HTMLButtonElement; - sourceColorIndex: number; - targetColorIndex: number; - } - >(); - private readonly colorReactionSwatches: Array<{ - colorIndex: number; - element: HTMLElement; - }> = []; private readonly state: PaneState = { backgroundColor: rgbColorToHex(activeVibe.backgroundColor), color1: rgbColorToHex(activeVibe.colors[0]), @@ -151,6 +95,9 @@ export class ConfigPane { }; public constructor(private readonly options: ConfigPaneOptions) { + this.colorReactionMatrix = new ColorReactionMatrixControl( + this.options.onRuntimeChange + ); this.container = document.createElement('div'); this.container.className = 'config-pane-container'; @@ -191,7 +138,7 @@ export class ConfigPane { public refresh(): void { this.syncVibeState(); this.pane.refresh(); - this.syncColorReactionMatrix(); + this.colorReactionMatrix.sync(); this.syncOpenState(); } @@ -237,7 +184,7 @@ export class ConfigPane { this.setUpVibeSection(container); this.addRuntimeSection(container, runtimeFolderOrder[0], true); this.addRuntimeSection(container, runtimeFolderOrder[1], true); - this.addColorReactionMatrix(container); + this.colorReactionMatrix.addTo(container); this.addRuntimeSection(container, runtimeFolderOrder[2], true); const performanceFolder = this.addRuntimeSection( container, @@ -246,7 +193,7 @@ export class ConfigPane { ); this.addFpsOverlayBinding(performanceFolder); this.setUpMusicSection(container); - this.syncColorReactionMatrix(); + this.colorReactionMatrix.sync(); } private setUpVibeSection(container: PaneContainer): void { @@ -295,7 +242,7 @@ export class ConfigPane { } updateColor(color); - this.syncColorReactionMatrix(); + this.colorReactionMatrix.sync(); this.options.onConfigChange(); }); } @@ -352,132 +299,6 @@ export class ConfigPane { .on('change', () => this.options.onConfigChange()); } - private addColorReactionMatrix(container: PaneContainer): void { - const folder = container.addFolder({ - title: 'Color Behavior', - expanded: true, - }); - - const matrix = document.createElement('div'); - matrix.className = 'color-reaction-matrix'; - - matrix.appendChild(this.createColorReactionCorner()); - colorReactionRows.forEach((row) => { - matrix.appendChild(this.createColorReactionHeader(row.colorIndex, row.label)); - }); - - colorReactionRows.forEach((row) => { - matrix.appendChild(this.createColorReactionHeader(row.colorIndex, row.label)); - row.keys.forEach((key, columnIndex) => { - matrix.appendChild( - this.createColorReactionCell(key, row.colorIndex, columnIndex) - ); - }); - }); - - const matrixBlade = folder.addBlade({ view: 'separator' }); - matrixBlade.element.classList.add('color-reaction-matrix-blade'); - matrixBlade.element.replaceChildren(matrix); - this.syncColorReactionMatrix(); - } - - private createColorReactionCorner(): HTMLDivElement { - const corner = document.createElement('div'); - corner.className = 'color-reaction-matrix__corner'; - return corner; - } - - private createColorReactionHeader(colorIndex: number, label: string): HTMLDivElement { - const header = document.createElement('div'); - header.className = 'color-reaction-matrix__header'; - header.setAttribute('aria-label', label); - header.title = label; - - const swatch = document.createElement('span'); - swatch.className = 'color-reaction-matrix__swatch'; - this.colorReactionSwatches.push({ colorIndex, element: swatch }); - header.appendChild(swatch); - - return header; - } - - private createColorReactionCell( - key: ColorReactionKey, - sourceColorIndex: number, - targetColorIndex: number - ): HTMLDivElement { - const cell = document.createElement('div'); - cell.className = 'color-reaction-matrix__cell'; - - const config = appConfig.runtimeSettings.controls[key]; - if (!config) { - return cell; - } - - const button = document.createElement('button'); - button.className = 'color-reaction-matrix__button'; - button.type = 'button'; - - const icon = document.createElement('span'); - icon.className = 'color-reaction-matrix__icon'; - button.appendChild(icon); - - button.addEventListener('click', () => { - const currentValue = normalizeNumberControlValue(settings[key], config); - const nextState = getNextColorReactionState(currentValue); - settings[key] = nextState.value; - this.syncColorReactionButton(button, key, sourceColorIndex, targetColorIndex); - this.options.onRuntimeChange(); - }); - - this.colorReactionButtons.set(key, { - element: button, - sourceColorIndex, - targetColorIndex, - }); - cell.appendChild(button); - - return cell; - } - - private syncColorReactionMatrix(): void { - this.colorReactionButtons.forEach( - ({ element, sourceColorIndex, targetColorIndex }, key) => { - this.syncColorReactionButton(element, key, sourceColorIndex, targetColorIndex); - } - ); - - this.colorReactionSwatches.forEach(({ colorIndex, element }) => { - element.style.backgroundColor = rgbColorToCss(activeVibe.colors[colorIndex]); - }); - } - - private syncColorReactionButton( - button: HTMLButtonElement, - key: ColorReactionKey, - sourceColorIndex: number, - targetColorIndex: number - ): void { - const config = appConfig.runtimeSettings.controls[key]; - if (!config) { - return; - } - - settings[key] = normalizeNumberControlValue(settings[key], config); - - const state = getColorReactionState(settings[key]); - const nextState = getNextColorReactionState(settings[key]); - const sourceLabel = COLOR_REACTION_LABELS[sourceColorIndex]; - const targetLabel = COLOR_REACTION_LABELS[targetColorIndex]; - - button.dataset.reaction = state.id; - button.setAttribute( - 'aria-label', - `${sourceLabel} ${state.label.toLowerCase()} ${targetLabel.toLowerCase()} trails; click to switch to ${nextState.label.toLowerCase()}` - ); - button.title = `${sourceLabel}: ${state.label} ${targetLabel} trails`; - } - private setUpMusicSection(container: PaneContainer): void { const folder = container.addFolder({ title: 'Music', expanded: true }); MUSIC_CONTROLS.forEach(({ key, label, min, max, step }) => { diff --git a/src/page/eraser-size-control.ts b/src/page/eraser-size-control.ts index b5de8f7..1099821 100644 --- a/src/page/eraser-size-control.ts +++ b/src/page/eraser-size-control.ts @@ -26,14 +26,15 @@ export class EraserSizeControl { HTMLLabelElement ); private readonly slider = queryRequiredElement('.eraser-size-slider', HTMLInputElement); + private isActive = false; public constructor(private readonly options: EraserSizeControlOptions) { - this.control.addEventListener('pointerdown', this.options.onActivate); - this.control.addEventListener('click', this.options.onActivate); - this.slider.addEventListener('focus', this.options.onActivate); + this.control.addEventListener('pointerdown', this.activate); + this.control.addEventListener('click', this.activate); + this.slider.addEventListener('focus', this.activate); this.slider.addEventListener('input', () => { settings.eraserSize = clampEraserSize(Number(this.slider.value)); - this.options.onActivate(); + this.activate(); this.render(); this.options.onChange(); }); @@ -59,6 +60,25 @@ export class EraserSizeControl { ratio; this.control.style.setProperty('--eraser-progress', `${ratio * 100}%`); this.control.style.setProperty('--eraser-control-scale', scale.toFixed(3)); + this.syncActiveState(); this.options.getGame()?.updateEraserPreview(); } + + public setActive(isActive: boolean): void { + this.isActive = isActive; + this.syncActiveState(); + } + + private readonly activate = () => { + this.setActive(true); + this.options.onActivate(); + }; + + private syncActiveState(): void { + this.control.classList.toggle('active', this.isActive); + this.slider.setAttribute( + 'aria-label', + this.isActive ? 'Eraser size, active' : 'Eraser size' + ); + } } diff --git a/src/page/full-screen-handler.ts b/src/page/full-screen-handler.ts index 1167543..498c08c 100644 --- a/src/page/full-screen-handler.ts +++ b/src/page/full-screen-handler.ts @@ -1,4 +1,6 @@ export class FullScreenHandler { + private readonly abortController = new AbortController(); + public constructor( private readonly toggleButton: HTMLElement, target: HTMLElement @@ -10,26 +12,35 @@ export class FullScreenHandler { this.updateButtons(); - addEventListener('fullscreenchange', this.updateButtons.bind(this)); - toggleButton.addEventListener('click', () => { - if (FullScreenHandler.isInFullScreenMode()) { - void document.exitFullscreen(); - return; - } + const { signal } = this.abortController; + addEventListener('fullscreenchange', this.updateButtons, { signal }); + toggleButton.addEventListener( + 'click', + () => { + if (FullScreenHandler.isInFullScreenMode()) { + void document.exitFullscreen(); + return; + } - void target.requestFullscreen().catch(() => undefined); - }); + void target.requestFullscreen().catch(() => undefined); + }, + { signal } + ); } public static isInFullScreenMode(): boolean { return document.fullscreenElement !== null; } - private updateButtons(): void { + public destroy(): void { + this.abortController.abort(); + } + + private readonly updateButtons = (): void => { const isInFullScreenMode = FullScreenHandler.isInFullScreenMode(); const label = isInFullScreenMode ? 'Exit fullscreen' : 'Enter fullscreen'; this.toggleButton.classList.toggle('active', isInFullScreenMode); this.toggleButton.setAttribute('aria-label', label); this.toggleButton.title = label; - } + }; } diff --git a/src/page/menu-hider.ts b/src/page/menu-hider.ts index 841ac48..ed8aa59 100644 --- a/src/page/menu-hider.ts +++ b/src/page/menu-hider.ts @@ -99,18 +99,18 @@ export class MenuHider { } private reveal(): void { + if (!this.isHidden && this.hideTimeout === undefined) { + return; + } + this.clearHideTimeout(); this.isHidden = false; this.element.classList.remove('menu-hidden'); - this.element.setAttribute('aria-hidden', 'false'); - this.element.inert = false; } private hide(): void { this.isHidden = true; this.element.classList.add('menu-hidden'); - this.element.setAttribute('aria-hidden', 'true'); - this.element.inert = true; } private clearHideTimeout(): void { diff --git a/src/page/mirror-segment-control.ts b/src/page/mirror-segment-control.ts index 98f6b75..1ea8ddc 100644 --- a/src/page/mirror-segment-control.ts +++ b/src/page/mirror-segment-control.ts @@ -14,7 +14,7 @@ const getMirrorSegmentRatio = (count: number): number => { }; const formatMirrorSegmentCount = (count: number): string => - count === appConfig.toolbar.mirror.default + count <= 1 ? appConfig.toolbar.mirror.offLabel : `${count} ${ appConfig.toolbar.mirror.names[ diff --git a/src/page/palette-control.ts b/src/page/palette-control.ts index 615625f..9007d8f 100644 --- a/src/page/palette-control.ts +++ b/src/page/palette-control.ts @@ -1,20 +1,16 @@ import type GameLoop from '../game-loop/game-loop'; import { activeVibe, settings } from '../settings'; -import { queryRequiredElement } from '../utils/dom'; import { ErrorCode, RuntimeError } from '../utils/error-handler'; import { rgbColorToCss } from '../utils/rgb-color'; interface PaletteControlOptions { getGame: () => GameLoop | null; onChange: () => void; + onModeChange?: (isEraserActive: boolean) => void; } export class PaletteControl { private readonly swatches = queryRequiredColorSwatches(); - private readonly eraserControl = queryRequiredElement( - '.eraser-size-control', - HTMLLabelElement - ); private isEraserActiveState = false; public constructor(private readonly options: PaletteControlOptions) { @@ -23,6 +19,7 @@ export class PaletteControl { settings.selectedColorIndex = index; this.isEraserActiveState = false; this.render(); + this.options.onModeChange?.(false); this.options.onChange(); }); }); @@ -35,17 +32,16 @@ export class PaletteControl { public setEraserActive(active: boolean): void { this.isEraserActiveState = active; this.render(); + this.options.onModeChange?.(active); } public render(): void { this.swatches.forEach((swatch, index) => { swatch.style.backgroundColor = rgbColorToCss(activeVibe.colors[index]); - swatch.classList.toggle( - 'active', - settings.selectedColorIndex === index && !this.isEraserActiveState - ); + const isActive = settings.selectedColorIndex === index && !this.isEraserActiveState; + swatch.classList.toggle('active', isActive); + swatch.setAttribute('aria-pressed', String(isActive)); }); - this.eraserControl.classList.toggle('active', this.isEraserActiveState); this.options.getGame()?.setEraseMode(this.isEraserActiveState); document.documentElement.style.setProperty( '--garden-background', diff --git a/src/page/vibe-navigator.ts b/src/page/vibe-navigator.ts index f784e78..fc6ecc0 100644 --- a/src/page/vibe-navigator.ts +++ b/src/page/vibe-navigator.ts @@ -15,6 +15,7 @@ interface VibeNavigatorOptions { } export class VibeNavigator { + private readonly abortController = new AbortController(); private readonly previousButton = queryRequiredElement( '.previous-vibe', HTMLButtonElement @@ -25,11 +26,20 @@ export class VibeNavigator { rememberActiveVibeSelection(); writeCurrentVibeUri(activeVibe.id, 'replace'); - this.previousButton.addEventListener('click', () => - this.select(-1, 'previous-button') + const { signal } = this.abortController; + this.previousButton.addEventListener( + 'click', + () => this.select(-1, 'previous-button'), + { signal } ); - this.nextButton.addEventListener('click', () => this.select(1, 'next-button')); - window.addEventListener('popstate', () => this.selectFromCurrentUri()); + this.nextButton.addEventListener('click', () => this.select(1, 'next-button'), { + signal, + }); + window.addEventListener('popstate', this.selectFromCurrentUri, { signal }); + } + + public destroy(): void { + this.abortController.abort(); } private select(offset: number, source: string): void { @@ -42,7 +52,7 @@ export class VibeNavigator { this.notifyChange(activePreset, source, true); } - private selectFromCurrentUri(): void { + private readonly selectFromCurrentUri = (): void => { const vibeId = getCurrentUriVibeId(); if (!vibeId || vibeId === activeVibe.id) { writeCurrentVibeUri(activeVibe.id, 'replace'); @@ -58,7 +68,7 @@ export class VibeNavigator { const activePreset = applyVibeSettings(vibe); writeCurrentVibeUri(activePreset.id, 'replace'); this.notifyChange(activePreset, 'uri-popstate', false); - } + }; private notifyChange( activePreset: typeof activeVibe, diff --git a/src/pipelines/agents/agent-generation/agent-compaction.wgsl b/src/pipelines/agents/agent-generation/agent-compaction.wgsl index 86a3447..ae9336c 100644 --- a/src/pipelines/agents/agent-generation/agent-compaction.wgsl +++ b/src/pipelines/agents/agent-generation/agent-compaction.wgsl @@ -9,7 +9,7 @@ struct Counters { aliveAgentCount: atomic, }; -const clearCompactedTailStride = 4u; +const clearCompactedTailStride = __CLEAR_COMPACTED_TAIL_STRIDE__u; @group(1) @binding(0) var settings: Settings; @group(1) @binding(2) var counters: Counters; @@ -30,7 +30,7 @@ fn main( var isAlive = false; var agent: Agent; if id < settings.agentCount { - isAlive = agents[id].colorIndex >= 0.0; + isAlive = agents[id].colorIndex >= 0.0 && agents[id].colorIndex < 2.5; if isAlive { agent = agents[id]; } diff --git a/src/pipelines/agents/agent-generation/agent-generation-pipeline.ts b/src/pipelines/agents/agent-generation/agent-generation-pipeline.ts index 9b62fbb..524a5c2 100644 --- a/src/pipelines/agents/agent-generation/agent-generation-pipeline.ts +++ b/src/pipelines/agents/agent-generation/agent-generation-pipeline.ts @@ -20,7 +20,7 @@ export class AgentGenerationPipeline { private readonly bindGroupLayout: GPUBindGroupLayout; private readonly uniforms: GPUBuffer; - private readonly bindGroupCache = createBindGroupCache( + private readonly bindGroupCache = createBindGroupCache<[GPUBuffer, GPUBuffer]>( (active, inactive) => this.device.createBindGroup({ layout: this.bindGroupLayout, @@ -130,7 +130,14 @@ export class AgentGenerationPipeline { }, }); - const compactionModule = smartCompile(device, compactionSchema, compactionShader); + const compactionModule = smartCompile( + device, + compactionSchema, + compactionShader.replaceAll( + '__CLEAR_COMPACTED_TAIL_STRIDE__', + AgentGenerationPipeline.CLEAR_COMPACTED_TAIL_STRIDE.toString() + ) + ); this.compactionPipeline = device.createComputePipeline({ layout: device.createPipelineLayout({ diff --git a/src/pipelines/agents/agent-pipeline.ts b/src/pipelines/agents/agent-pipeline.ts index 4d4dd0d..6138935 100644 --- a/src/pipelines/agents/agent-pipeline.ts +++ b/src/pipelines/agents/agent-pipeline.ts @@ -1,4 +1,4 @@ -import { createBindGroupCache3 } from '../../utils/graphics/bind-group-cache'; +import { createBindGroupCache } from '../../utils/graphics/bind-group-cache'; import { createCachedBufferWrite, writeBufferIfChanged, @@ -64,10 +64,8 @@ export class AgentPipeline { private readonly uniformCache = createCachedBufferWrite( UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT ); - private readonly bindGroupCache = createBindGroupCache3< - GPUBuffer, - GPUTextureView, - GPUTextureView + private readonly bindGroupCache = createBindGroupCache< + [GPUBuffer, GPUTextureView, GPUTextureView] >((agentsBuffer, trailMapIn, trailMapOut) => this.device.createBindGroup({ layout: this.bindGroupLayout, diff --git a/src/pipelines/agents/agent.wgsl b/src/pipelines/agents/agent.wgsl index 591fa10..912e2a0 100644 --- a/src/pipelines/agents/agent.wgsl +++ b/src/pipelines/agents/agent.wgsl @@ -212,10 +212,10 @@ fn agent_finalize( rotation = PI + random_float(randomSeed + 22695477u) - 0.5; } - // Writes only the deposit into a per-frame-cleared depositMap. The diffusion - // pass sums trailMap + depositMap at tile-load time, so the previous trail - // value is no longer needed here. Alpha stays 0 in depositMap — diffuse's - // alpha decay reads it from trailMap (where deposit alpha contributes 0). + // Writes only this agent's last-writer-wins deposit into a per-frame-cleared + // depositMap. Storage textures do not blend concurrent compute writes, so + // overlapping agents intentionally collapse to whichever write wins. The + // diffusion pass then sums trailMap + depositMap at tile-load time. textureStore( trailMapOut, vec2(nextPosition), diff --git a/src/pipelines/brush/brush-pipeline.ts b/src/pipelines/brush/brush-pipeline.ts index 7302f14..b759c4f 100644 --- a/src/pipelines/brush/brush-pipeline.ts +++ b/src/pipelines/brush/brush-pipeline.ts @@ -122,7 +122,7 @@ export class BrushPipeline { }, fragment: { module: shaderModule, - entryPoint: 'fragmentMrt', + entryPoint: 'fragment', targets: [ { format: TRAIL_SOURCE_TEXTURE_FORMAT, @@ -166,7 +166,7 @@ export class BrushPipeline { this.segments.flush(); } - public executeMultiTarget( + public executeSource( commandEncoder: GPUCommandEncoder, sourceMapOut: GPUTextureView, timestampWrites?: GPURenderPassTimestampWrites @@ -176,6 +176,7 @@ export class BrushPipeline { return false; } + recordBrushPassForE2e(); const passEncoder = commandEncoder.beginRenderPass({ colorAttachments: [{ view: sourceMapOut, loadOp: 'load', storeOp: 'store' }], timestampWrites, @@ -194,3 +195,12 @@ export class BrushPipeline { this.uniforms.destroy(); } } + +const recordBrushPassForE2e = (): void => { + if (typeof window === 'undefined') { + return; + } + + const state = window as Window & { __fleetingGardenBrushPasses?: number }; + state.__fleetingGardenBrushPasses = (state.__fleetingGardenBrushPasses ?? 0) + 1; +}; diff --git a/src/pipelines/brush/brush.wgsl b/src/pipelines/brush/brush.wgsl index b12f376..8152986 100644 --- a/src/pipelines/brush/brush.wgsl +++ b/src/pipelines/brush/brush.wgsl @@ -50,7 +50,7 @@ fn vertex( } @fragment -fn fragmentMrt( +fn fragment( @location(0) screenPosition: vec2, @location(1) @interpolate(flat) start: vec2, @location(2) @interpolate(flat) direction: vec2, diff --git a/src/pipelines/diffusion/diffuse.wgsl b/src/pipelines/diffusion/diffuse.wgsl index 57bf139..92ae448 100644 --- a/src/pipelines/diffusion/diffuse.wgsl +++ b/src/pipelines/diffusion/diffuse.wgsl @@ -32,8 +32,8 @@ const HASH_TO_UNIT_FLOAT: f32 = 2.3283064365386963e-10; // in the same frame. @group(0) @binding(3) var depositMap: texture_2d; -var tile: array, 324>; -var tileTrailStrength: array; +var tile: array, TILE_TEXEL_COUNT>; +var tileTrailStrength: array; @compute @workgroup_size(__WORKGROUP_SIZE__, __WORKGROUP_SIZE__) fn main( diff --git a/src/pipelines/diffusion/diffusion-pipeline.ts b/src/pipelines/diffusion/diffusion-pipeline.ts index 373e736..b1c763d 100644 --- a/src/pipelines/diffusion/diffusion-pipeline.ts +++ b/src/pipelines/diffusion/diffusion-pipeline.ts @@ -1,7 +1,7 @@ import { vec2 } from 'gl-matrix'; import { appConfig } from '../../config'; -import { createBindGroupCache3 } from '../../utils/graphics/bind-group-cache'; +import { createBindGroupCache } from '../../utils/graphics/bind-group-cache'; import { createCachedBufferWrite, writeBufferIfChanged, @@ -78,10 +78,8 @@ export class DiffusionPipeline { private readonly uniformCache = createCachedBufferWrite( DiffusionPipeline.UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT ); - private readonly getBindGroup = createBindGroupCache3< - GPUTextureView, - GPUTextureView, - GPUTextureView + private readonly getBindGroup = createBindGroupCache< + [GPUTextureView, GPUTextureView, GPUTextureView] >((trailMapIn, trailMapOut, depositMap) => this.device.createBindGroup({ layout: this.bindGroupLayout, diff --git a/src/pipelines/eraser/eraser-agent-pipeline.ts b/src/pipelines/eraser/eraser-agent-pipeline.ts index 4a85c14..5cc9b32 100644 --- a/src/pipelines/eraser/eraser-agent-pipeline.ts +++ b/src/pipelines/eraser/eraser-agent-pipeline.ts @@ -32,7 +32,7 @@ export class EraserAgentPipeline { private readonly uniformCache = createCachedBufferWrite( EraserAgentPipeline.UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT ); - private readonly bindGroupCache = createBindGroupCache( + private readonly bindGroupCache = createBindGroupCache<[GPUBuffer, GPUTextureView]>( (agentsBuffer, eraserMask) => this.device.createBindGroup({ layout: this.bindGroupLayout, diff --git a/src/pipelines/eraser/eraser-agent.wgsl b/src/pipelines/eraser/eraser-agent.wgsl index 273ffbc..f98d551 100644 --- a/src/pipelines/eraser/eraser-agent.wgsl +++ b/src/pipelines/eraser/eraser-agent.wgsl @@ -21,7 +21,7 @@ fn main( } let colorIndex = agents[id].colorIndex; - if colorIndex < 0.0 { + if colorIndex < 0.0 || colorIndex >= 2.5 { return; } diff --git a/src/pipelines/render/render-pipeline.ts b/src/pipelines/render/render-pipeline.ts index 57f0b77..12d7ada 100644 --- a/src/pipelines/render/render-pipeline.ts +++ b/src/pipelines/render/render-pipeline.ts @@ -6,7 +6,6 @@ import { import { setUpFullScreenQuad } from '../../utils/graphics/full-screen-quad'; import { smartCompile } from '../../utils/graphics/smart-compile'; import { rgbChannelToUnit, type RgbColor } from '../../utils/rgb-color'; -import { CommonState } from '../common-state/common-state'; import shader from './render.wgsl?raw'; export interface RenderSettings { @@ -30,7 +29,7 @@ export class RenderPipeline { UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT ); - private readonly getBindGroup = createBindGroupCache( + private readonly getBindGroup = createBindGroupCache<[GPUTextureView, GPUTextureView]>( (colorTexture, sourceTexture) => this.device.createBindGroup({ layout: this.bindGroupLayout, @@ -45,7 +44,6 @@ export class RenderPipeline { public constructor( private readonly context: GPUCanvasContext, private readonly device: GPUDevice, - private readonly commonState: CommonState, private readonly canvasFormat: GPUTextureFormat ) { this.bindGroupLayout = device.createBindGroupLayout({ @@ -68,10 +66,10 @@ export class RenderPipeline { ], }); - const shaderModule = smartCompile(device, CommonState.shaderCode, shader); + const shaderModule = smartCompile(device, shader); const vertex = setUpFullScreenQuad(device); const pipelineLayout = device.createPipelineLayout({ - bindGroupLayouts: [this.commonState.bindGroupLayout, this.bindGroupLayout], + bindGroupLayouts: [this.bindGroupLayout], }); this.pipeline = this.createPipeline( pipelineLayout, @@ -207,8 +205,7 @@ export class RenderPipeline { timestampWrites, }); passEncoder.setPipeline(this.getPipeline(useSourceTexture)); - this.commonState.execute(passEncoder); - passEncoder.setBindGroup(1, this.getBindGroup(colorTexture, sourceTexture)); + passEncoder.setBindGroup(0, this.getBindGroup(colorTexture, sourceTexture)); passEncoder.draw(3, 1); passEncoder.end(); } diff --git a/src/pipelines/render/render.wgsl b/src/pipelines/render/render.wgsl index d5613bd..7e1ab26 100644 --- a/src/pipelines/render/render.wgsl +++ b/src/pipelines/render/render.wgsl @@ -20,9 +20,9 @@ const LOW_SATURATION_RESCUE_MAX: f32 = 0.22; const COLOR_WEIGHT_EPSILON: f32 = 0.0001; const LUMA_WEIGHTS: vec3 = vec3(0.2126, 0.7152, 0.0722); -@group(1) @binding(0) var settings: Settings; -@group(1) @binding(2) var trailMap: texture_2d; -@group(1) @binding(3) var sourceMap: texture_2d; +@group(0) @binding(0) var settings: Settings; +@group(0) @binding(2) var trailMap: texture_2d; +@group(0) @binding(3) var sourceMap: texture_2d; @fragment fn fragment(@builtin(position) position: vec4) -> @location(0) vec4 { diff --git a/src/settings.ts b/src/settings.ts index 49e2518..c91ac83 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -18,12 +18,20 @@ const preservedRuntimeSettingKeys = [ const cloneRgbColor = (color: T): T => [...color] as T; +const cloneVibeAudio = (audio: VibePreset['audio']): VibePreset['audio'] => ({ + ...audio, + ...(audio.scale ? { scale: [...audio.scale] } : {}), + ...(audio.progression + ? { progression: audio.progression.map((chord) => ({ ...chord })) } + : {}), +}); + const cloneVibePreset = (vibe: VibePreset): VibePreset => ({ ...vibe, colors: vibe.colors.map(cloneRgbColor) as VibePreset['colors'], backgroundColor: cloneRgbColor(vibe.backgroundColor), settings: { ...vibe.settings }, - audio: { ...vibe.audio }, + audio: cloneVibeAudio(vibe.audio), }); const buildSettings = (vibe: VibePreset): GardenRuntimeSettings => diff --git a/src/style/_config-pane.scss b/src/style/_config-pane.scss index 71ed186..ad5402f 100644 --- a/src/style/_config-pane.scss +++ b/src/style/_config-pane.scss @@ -1,3 +1,5 @@ +@use 'mixins' as *; + .config-pane-container { --config-pane-available-height: calc( 100vh - 24px - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px) @@ -154,7 +156,7 @@ } } -@media (max-width: 599px), (hover: none) and (pointer: coarse) { +@include on-mobile-input { @include mobile-config-pane; } diff --git a/src/style/mixins.scss b/src/style/mixins.scss index 46f2dd9..9197605 100644 --- a/src/style/mixins.scss +++ b/src/style/mixins.scss @@ -5,3 +5,9 @@ $breakpoint-width: 600px !default; @content; } } + +@mixin on-mobile-input() { + @media (max-width: ($breakpoint-width - 1px)), (hover: none) and (pointer: coarse) { + @content; + } +} diff --git a/src/style/toolbar/_layout.scss b/src/style/toolbar/_layout.scss index e11a841..f2c8930 100644 --- a/src/style/toolbar/_layout.scss +++ b/src/style/toolbar/_layout.scss @@ -86,7 +86,13 @@ } > .vibe-button { + --vibe-button-surface-inset-block: 10px; + --vibe-button-surface-inset-inline: 8px; + --vibe-chevron-size: 22px; + --vibe-chevron-stroke: 4px; + position: relative; + isolation: isolate; display: grid; place-items: center; width: var(--vibe-button-hit-size); @@ -96,30 +102,56 @@ padding: 0; border-radius: 0; background: transparent; - color: rgb(255 255 255 / 70%); + color: rgb(255 255 255 / 88%); font-size: 0; line-height: 1; + &::after { + content: ''; + position: absolute; + z-index: 0; + inset: var(--vibe-button-surface-inset-block) + var(--vibe-button-surface-inset-inline); + border-radius: 8px; + background: rgb(255 255 255 / calc(9% + var(--toolbar-background-strength) * 10%)); + box-shadow: + inset 0 0 0 1px rgb(255 255 255 / 18%), + 0 8px 18px rgb(0 0 0 / calc(var(--toolbar-background-strength) * 22%)); + transition: + background var(--transition-time), + box-shadow var(--transition-time), + opacity var(--transition-time); + } + &::before { content: ''; position: absolute; + z-index: 1; top: 50%; left: 50%; - width: 18px; - height: 18px; + width: var(--vibe-chevron-size); + height: var(--vibe-chevron-size); border-color: currentColor; border-style: solid; - border-width: 0 0 3px 3px; + border-width: 0 0 var(--vibe-chevron-stroke) var(--vibe-chevron-stroke); + filter: drop-shadow(0 1px 3px rgb(0 0 0 / 70%)); transform: translate(-35%, -50%) rotate(45deg); } &.next-vibe::before { - border-width: 3px 3px 0 0; + border-width: var(--vibe-chevron-stroke) var(--vibe-chevron-stroke) 0 0; transform: translate(-65%, -50%) rotate(45deg); } &:hover { - color: color-mix(in srgb, var(--accent-color) 70%, white); + color: white; + } + + &:hover::after { + background: color-mix(in srgb, var(--accent-color) 34%, rgb(255 255 255 / 18%)); + box-shadow: + inset 0 0 0 1px rgb(255 255 255 / 28%), + 0 10px 22px rgb(0 0 0 / calc(var(--toolbar-background-strength) * 30%)); } &.previous-vibe:hover { diff --git a/src/style/toolbar/_responsive.scss b/src/style/toolbar/_responsive.scss index 658c40b..2aeba6c 100644 --- a/src/style/toolbar/_responsive.scss +++ b/src/style/toolbar/_responsive.scss @@ -16,13 +16,13 @@ row-gap: 0; > .vibe-button { + --vibe-button-surface-inset-block: 5px; + --vibe-button-surface-inset-inline: 3px; + --vibe-chevron-size: 17px; + --vibe-chevron-stroke: 3px; + width: var(--vibe-button-hit-size); min-height: 44px; - - &::before { - width: 14px; - height: 14px; - } } > .toolbar-shell { diff --git a/src/utils/error-handler.ts b/src/utils/error-handler.ts index 328f329..cffd60c 100644 --- a/src/utils/error-handler.ts +++ b/src/utils/error-handler.ts @@ -223,7 +223,12 @@ export class ErrorHandler { public static addOnErrorListener( listener: (error: ErrorHandlerError, metadata: ErrorMetadata) => void - ) { + ): () => void { ErrorHandler.onErrorListeners.push(listener); + return () => { + ErrorHandler.onErrorListeners = ErrorHandler.onErrorListeners.filter( + (registeredListener) => registeredListener !== listener + ); + }; } } diff --git a/src/utils/graphics/bind-group-cache.ts b/src/utils/graphics/bind-group-cache.ts index 28ecb84..40ebce0 100644 --- a/src/utils/graphics/bind-group-cache.ts +++ b/src/utils/graphics/bind-group-cache.ts @@ -1,48 +1,38 @@ -export const createBindGroupCache = ( - factory: (key1: K1, key2: K2) => GPUBindGroup -): ((key1: K1, key2: K2) => GPUBindGroup) => { - const outer = new WeakMap>(); - return (key1, key2) => { - let inner = outer.get(key1); - if (!inner) { - inner = new WeakMap(); - outer.set(key1, inner); - } - const cached = inner.get(key2); - if (cached) { - return cached; - } - const bindGroup = factory(key1, key2); - inner.set(key2, bindGroup); - return bindGroup; - }; +type BindGroupCacheKeys = readonly [object, ...object[]]; + +interface BindGroupCacheNode { + bindGroup?: GPUBindGroup; + children: WeakMap; +} + +const createNode = (): BindGroupCacheNode => ({ + children: new WeakMap(), +}); + +const getOrCreateNode = ( + children: WeakMap, + key: object +): BindGroupCacheNode => { + let node = children.get(key); + if (!node) { + node = createNode(); + children.set(key, node); + } + return node; }; -export const createBindGroupCache3 = < - K1 extends object, - K2 extends object, - K3 extends object, ->( - factory: (key1: K1, key2: K2, key3: K3) => GPUBindGroup -): ((key1: K1, key2: K2, key3: K3) => GPUBindGroup) => { - const outer = new WeakMap>>(); - return (key1, key2, key3) => { - let mid = outer.get(key1); - if (!mid) { - mid = new WeakMap(); - outer.set(key1, mid); +export const createBindGroupCache = ( + factory: (...keys: Keys) => GPUBindGroup +): ((...keys: Keys) => GPUBindGroup) => { + const root = new WeakMap(); + + return (...keys) => { + let node = getOrCreateNode(root, keys[0]); + for (const key of keys.slice(1)) { + node = getOrCreateNode(node.children, key); } - let inner = mid.get(key2); - if (!inner) { - inner = new WeakMap(); - mid.set(key2, inner); - } - const cached = inner.get(key3); - if (cached) { - return cached; - } - const bindGroup = factory(key1, key2, key3); - inner.set(key3, bindGroup); - return bindGroup; + + node.bindGroup ??= factory(...keys); + return node.bindGroup; }; }; diff --git a/src/vibe-uri.test.ts b/src/vibe-uri.test.ts index 79f0109..fe55d5e 100644 --- a/src/vibe-uri.test.ts +++ b/src/vibe-uri.test.ts @@ -8,10 +8,10 @@ describe('vibe URI handling', () => { expect(getVibeIdFromUri('https://example.test/?vibe=aurora-mycelium')).toBe( VibeId.AuroraMycelium ); - expect(getVibeIdFromUri('https://example.test/?vibe=Aurora%20Mycelium%20Copy')).toBe( + expect(getVibeIdFromUri('https://example.test/?vibe=Aurora%20Mycelium')).toBe( VibeId.AuroraMycelium ); - expect(getVibeIdFromUri('https://example.test/?vibe=velvet%20observatory')).toBe( + expect(getVibeIdFromUri('https://example.test/?vibe=Velvet%20Observatory%20Copy')).toBe( VibeId.VelvetObservatory ); }); diff --git a/src/vibe-uri.ts b/src/vibe-uri.ts index c8ee804..3a4024c 100644 --- a/src/vibe-uri.ts +++ b/src/vibe-uri.ts @@ -1,5 +1,5 @@ import type { VibeId } from './config/types'; -import { vibePresets } from './config/vibe-presets'; +import { getVibeById, VIBE_PRESETS } from './vibe-registry'; const VIBE_URI_QUERY_PARAM = 'vibe'; const FALLBACK_URL_ORIGIN = 'https://fleeting.garden'; @@ -27,7 +27,7 @@ const normalizeVibeIdentifier = (value: string): string => const vibeIdByIdentifier = new Map(); -for (const vibe of vibePresets) { +for (const vibe of VIBE_PRESETS) { vibeIdByIdentifier.set(normalizeVibeIdentifier(vibe.id), vibe.id); vibeIdByIdentifier.set(normalizeVibeIdentifier(vibe.name), vibe.id); } @@ -106,8 +106,8 @@ export const getCurrentUriVibeId = (): VibeId | null => { }; const getVibeSlug = (vibeId: VibeId): string => { - const vibe = vibePresets.find((preset) => preset.id === vibeId); - return vibe ? slugifyVibeName(vibe.name) : vibeId; + const vibe = getVibeById(vibeId); + return vibe ? vibe.id : vibeId; }; export const createVibeUri = (url: string | URL, vibeId: VibeId): string => { diff --git a/src/vibes.ts b/src/vibes.ts index def4584..95d1f7c 100644 --- a/src/vibes.ts +++ b/src/vibes.ts @@ -1,20 +1,18 @@ import { appConfig } from './config'; import { VibeId, type VibePreset } from './config/types'; import { readBrowserStorage } from './utils/browser-storage'; +import { getVibeById, VIBE_PRESETS } from './vibe-registry'; import { getCurrentUriVibeId, getVibeIdFromUri } from './vibe-uri'; export { VibeId }; +export { getVibeById, VIBE_PRESETS }; export type { VibePreset }; -export const VIBE_PRESETS: Array = appConfig.vibes.presets; const VIBE_IDS = new Set(VIBE_PRESETS.map((vibe) => vibe.id)); const isVibeId = (value: unknown): value is VibeId => typeof value === 'string' && VIBE_IDS.has(value as VibeId); -export const getVibeById = (vibeId: VibeId): VibePreset | undefined => - VIBE_PRESETS.find((vibe) => vibe.id === vibeId); - export const getInitialVibe = (): VibePreset => { const uriVibeId = getCurrentUriVibeId(); const storedVibeId = readBrowserStorage(appConfig.storage.vibeKey); diff --git a/tsconfig.json b/tsconfig.json index 09374bb..fbd104c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,5 +17,5 @@ "noUnusedLocals": true, "noUnusedParameters": true }, - "include": ["src/**/*", "definitions.d.ts", "vite.config.ts"] + "include": ["src/**/*", "pwa-assets.config.ts", "vite.config.ts"] }