From 42b32cb07de141580422c189172d83bfc241bee9 Mon Sep 17 00:00:00 2001 From: Till JS Date: Sun, 22 Mar 2026 19:16:34 +0100 Subject: [PATCH] feat(mukke): add pluggable visualizer system with Butterchurn and Particle modes Replace static FrequencyBars with VisualizerRenderer supporting multiple visualizer backends via registry. Add Butterchurn (Milkdrop) and Particle (pixi.js) visualizers with runtime switching. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/mukke/apps/web/package.json | 4 + apps/mukke/apps/web/src/app.d.ts | 39 +++ .../web/src/lib/components/FullPlayer.svelte | 6 +- .../src/lib/visualizer/ButterchurnViz.svelte | 182 ++++++++++++++ .../web/src/lib/visualizer/ParticleViz.svelte | 222 ++++++++++++++++++ .../lib/visualizer/VisualizerRenderer.svelte | 48 ++++ .../apps/web/src/lib/visualizer/analyzer.ts | 16 ++ .../web/src/lib/visualizer/registry.svelte.ts | 63 +++++ 8 files changed, 577 insertions(+), 3 deletions(-) create mode 100644 apps/mukke/apps/web/src/lib/visualizer/ButterchurnViz.svelte create mode 100644 apps/mukke/apps/web/src/lib/visualizer/ParticleViz.svelte create mode 100644 apps/mukke/apps/web/src/lib/visualizer/VisualizerRenderer.svelte create mode 100644 apps/mukke/apps/web/src/lib/visualizer/registry.svelte.ts diff --git a/apps/mukke/apps/web/package.json b/apps/mukke/apps/web/package.json index 084ffa140..bb6a2f7f4 100644 --- a/apps/mukke/apps/web/package.json +++ b/apps/mukke/apps/web/package.json @@ -40,6 +40,7 @@ "@manacore/shared-auth": "workspace:*", "@manacore/shared-auth-ui": "workspace:*", "@manacore/shared-branding": "workspace:*", + "@manacore/shared-error-tracking": "workspace:*", "@manacore/shared-feedback-service": "workspace:^", "@manacore/shared-feedback-ui": "workspace:^", "@manacore/shared-i18n": "workspace:*", @@ -52,6 +53,9 @@ "@manacore/shared-ui": "workspace:*", "@manacore/shared-utils": "workspace:*", "@mukke/shared": "workspace:*", + "butterchurn": "^2.6.7", + "butterchurn-presets": "^2.4.7", + "pixi.js": "^8.17.1", "wavesurfer.js": "^7.8.0" }, "type": "module" diff --git a/apps/mukke/apps/web/src/app.d.ts b/apps/mukke/apps/web/src/app.d.ts index c269fca6f..f2762c8d7 100644 --- a/apps/mukke/apps/web/src/app.d.ts +++ b/apps/mukke/apps/web/src/app.d.ts @@ -1,2 +1,41 @@ declare const __BUILD_HASH__: string; declare const __BUILD_TIME__: string; + +declare module 'butterchurn' { + interface ButterchurnVisualizer { + connectAudio(source: AudioNode): void; + disconnectAudio(source: AudioNode): void; + loadPreset(preset: Record, blendTime?: number): void; + render(opts?: Record): void; + setRendererSize(width: number, height: number, opts?: Record): void; + setInternalMeshSize(width: number, height: number): void; + setOutputAA(useAA: boolean): void; + setCanvas(canvas: HTMLCanvasElement): void; + launchSongTitleAnim(text: string): void; + toDataURL(): string; + loseGLContext(): void; + } + + interface ButterchurnOptions { + width: number; + height: number; + pixelRatio?: number; + textureRatio?: number; + } + + interface Butterchurn { + createVisualizer( + audioContext: AudioContext, + canvas: HTMLCanvasElement, + options: ButterchurnOptions + ): ButterchurnVisualizer; + } + + const butterchurn: Butterchurn; + export default butterchurn; +} + +declare module 'butterchurn-presets' { + function getPresets(): Record>; + export default getPresets; +} diff --git a/apps/mukke/apps/web/src/lib/components/FullPlayer.svelte b/apps/mukke/apps/web/src/lib/components/FullPlayer.svelte index 35e543fff..96ecfc68a 100644 --- a/apps/mukke/apps/web/src/lib/components/FullPlayer.svelte +++ b/apps/mukke/apps/web/src/lib/components/FullPlayer.svelte @@ -1,6 +1,6 @@ + +
+ + + {#if isInitialized} + +
+ + + + {currentPresetName} + + + +
+ {/if} +
diff --git a/apps/mukke/apps/web/src/lib/visualizer/ParticleViz.svelte b/apps/mukke/apps/web/src/lib/visualizer/ParticleViz.svelte new file mode 100644 index 000000000..f1ffa7284 --- /dev/null +++ b/apps/mukke/apps/web/src/lib/visualizer/ParticleViz.svelte @@ -0,0 +1,222 @@ + + + diff --git a/apps/mukke/apps/web/src/lib/visualizer/VisualizerRenderer.svelte b/apps/mukke/apps/web/src/lib/visualizer/VisualizerRenderer.svelte new file mode 100644 index 000000000..39f90e205 --- /dev/null +++ b/apps/mukke/apps/web/src/lib/visualizer/VisualizerRenderer.svelte @@ -0,0 +1,48 @@ + + +
+ {#if visualizerStore.active === 'bars'} + + {:else if visualizerStore.active === 'butterchurn'} + + {:else if visualizerStore.active === 'particles'} + + {/if} + + {#if showSwitcher} +
+ {#each visualizerStore.all as viz} + + {/each} +
+ {/if} +
diff --git a/apps/mukke/apps/web/src/lib/visualizer/analyzer.ts b/apps/mukke/apps/web/src/lib/visualizer/analyzer.ts index a0360bb5b..e30c234ea 100644 --- a/apps/mukke/apps/web/src/lib/visualizer/analyzer.ts +++ b/apps/mukke/apps/web/src/lib/visualizer/analyzer.ts @@ -68,6 +68,22 @@ export function getAnalyzer(): AnalyserNode | null { return analyserNode; } +/** + * Get the AudioContext (null if not yet created). + * Needed by Butterchurn for its own audio processing. + */ +export function getAudioContext(): AudioContext | null { + return audioContext; +} + +/** + * Get the MediaElementAudioSourceNode (null if not yet connected). + * Needed by Butterchurn to connect its own audio analysis. + */ +export function getSourceNode(): MediaElementAudioSourceNode | null { + return sourceNode; +} + /** * Resume the AudioContext (required after user gesture on some browsers). */ diff --git a/apps/mukke/apps/web/src/lib/visualizer/registry.svelte.ts b/apps/mukke/apps/web/src/lib/visualizer/registry.svelte.ts new file mode 100644 index 000000000..a08131273 --- /dev/null +++ b/apps/mukke/apps/web/src/lib/visualizer/registry.svelte.ts @@ -0,0 +1,63 @@ +export type VisualizerType = 'bars' | 'butterchurn' | 'particles'; + +export interface VisualizerInfo { + id: VisualizerType; + name: string; + description: string; + icon: string; +} + +export const VISUALIZERS: VisualizerInfo[] = [ + { + id: 'bars', + name: 'Frequency Bars', + description: 'Classic equalizer bars with frequency spectrum', + icon: '▊▊▊', + }, + { + id: 'butterchurn', + name: 'Milkdrop', + description: 'Classic Winamp/Milkdrop presets (500+ visuals)', + icon: '✦', + }, + { + id: 'particles', + name: 'Particles', + description: 'GPU-accelerated particle flow reacting to audio', + icon: '·:·', + }, +]; + +function createVisualizerStore() { + let activeId = $state('bars'); + + return { + get active() { + return activeId; + }, + get activeInfo() { + return VISUALIZERS.find((v) => v.id === activeId)!; + }, + get all() { + return VISUALIZERS; + }, + + setActive(id: VisualizerType) { + activeId = id; + }, + + next() { + const idx = VISUALIZERS.findIndex((v) => v.id === activeId); + const nextIdx = (idx + 1) % VISUALIZERS.length; + activeId = VISUALIZERS[nextIdx].id; + }, + + previous() { + const idx = VISUALIZERS.findIndex((v) => v.id === activeId); + const prevIdx = (idx - 1 + VISUALIZERS.length) % VISUALIZERS.length; + activeId = VISUALIZERS[prevIdx].id; + }, + }; +} + +export const visualizerStore = createVisualizerStore();