diff --git a/app/.eslintrc.cjs b/app/.eslintrc.cjs index 44eba40d..1420009b 100644 --- a/app/.eslintrc.cjs +++ b/app/.eslintrc.cjs @@ -25,6 +25,7 @@ const config = { "plugin:jsx-a11y/recommended", ], rules: { + "@typescript-eslint/consistent-type-definitions": "off", "@typescript-eslint/no-unused-vars": [ "error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, @@ -43,6 +44,7 @@ const config = { "jsx-a11y/no-static-element-interactions": "off", "jsx-a11y/heading-has-content": "off", "react/no-unknown-property": "off", + "react/display-name": "off", }, globals: { React: "writable", diff --git a/app/package.json b/app/package.json index 3268eb71..0723bb65 100644 --- a/app/package.json +++ b/app/package.json @@ -5,6 +5,7 @@ "private": true, "scripts": { "dev": "vite", + "mobile": "vite --host", "build": "tsc && vite build", "preview": "vite preview", "typecheck": "tsc --noEmit", @@ -31,6 +32,7 @@ "@tanstack/react-query": "5.56.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "framer-motion": "^11.11.7", "lucide-react": "^0.445.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index edaaae8d..339727fb 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -59,6 +59,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + framer-motion: + specifier: ^11.11.7 + version: 11.11.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) lucide-react: specifier: ^0.445.0 version: 0.445.0(react@18.3.1) @@ -1753,6 +1756,20 @@ packages: fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + framer-motion@11.11.7: + resolution: {integrity: sha512-89CgILOXPeG3L7ymOTGrLmf8IiKubYLUN/QkYgQuLvehAHfqgwJbLfCnhuyRI4WTds1TXkUp67A7IJrgRY/j1w==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 + react-dom: ^18.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + fs-extra@11.2.0: resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} engines: {node: '>=14.14'} @@ -4893,6 +4910,13 @@ snapshots: fraction.js@4.3.7: {} + framer-motion@11.11.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + tslib: 2.6.2 + optionalDependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + fs-extra@11.2.0: dependencies: graceful-fs: 4.2.11 diff --git a/app/src/App.tsx b/app/src/App.tsx index 89ab6b51..6ce084a1 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -3,12 +3,14 @@ import AudioAnalyzer from "@/components/analyzers/audioAnalyzer"; import AudioScopeCanvas from "@/components/canvas/AudioScope"; import Visual3DCanvas from "@/components/canvas/Visual3D"; import { ControlsPanel } from "@/components/controls/main"; -import { useModeContext } from "@/context/mode"; -import { APPLICATION_MODE, type ApplicationMode } from "@/lib/applicationModes"; +import { + APPLICATION_MODE, + type TApplicationMode, +} from "@/lib/applicationModes"; -import { useAppStateActions } from "./lib/appState"; +import { useAppStateActions, useMode } from "./lib/appState"; -const getAnalyzerComponent = (mode: ApplicationMode) => { +const getAnalyzerComponent = (mode: TApplicationMode) => { switch (mode) { case APPLICATION_MODE.AUDIO: case APPLICATION_MODE.AUDIO_SCOPE: @@ -22,7 +24,7 @@ const getAnalyzerComponent = (mode: ApplicationMode) => { } }; -const getCanvasComponent = (mode: ApplicationMode) => { +const getCanvasComponent = (mode: TApplicationMode) => { switch (mode) { case APPLICATION_MODE.AUDIO_SCOPE: return ; @@ -30,14 +32,14 @@ const getCanvasComponent = (mode: ApplicationMode) => { case APPLICATION_MODE.NOISE: case APPLICATION_MODE.AUDIO: case APPLICATION_MODE.PARTICLE_NOISE: - return ; + return ; default: return mode satisfies never; } }; const App = () => { - const { mode } = useModeContext(); + const mode = useMode(); const { noteCanvasInteraction } = useAppStateActions(); return ( diff --git a/app/src/components/analyzers/audioAnalyzer.tsx b/app/src/components/analyzers/audioAnalyzer.tsx index a146430d..3814f329 100644 --- a/app/src/components/analyzers/audioAnalyzer.tsx +++ b/app/src/components/analyzers/audioAnalyzer.tsx @@ -5,89 +5,113 @@ import { AUDIO_SOURCE, buildAudio, buildAudioContext, + type TAudioSource, } from "@/components/audio/sourceControls/common"; import MicrophoneAudioControls from "@/components/audio/sourceControls/mic"; import ScreenShareControls from "@/components/audio/sourceControls/screenshare"; -import { useAudioSourceContext } from "@/context/audioSource"; -import { useMediaStreamLink } from "@/lib/analyzers/common"; +import { + useMediaStreamLink, + type TAnalyzerInputControl, +} from "@/lib/analyzers/common"; import FFTAnalyzer from "@/lib/analyzers/fft"; import ScopeAnalyzer from "@/lib/analyzers/scope"; import { APPLICATION_MODE } from "@/lib/applicationModes"; +import { useAudio } from "@/lib/appState"; import { AudioScopeAnalyzerControls } from "./scopeAnalyzerControls"; -const InternalAudioAnalyzer = ({ - mode, - audioSource, +const buildScopeAnalyzer = () => { + const audioCtx = buildAudioContext(); + const audio = buildAudio(); + return { + audio, + analyzer: new ScopeAnalyzer(audio, audioCtx), + }; +}; + +const buildFFTAnalyzer = (volume: number) => { + const audioCtx = buildAudioContext(); + const audio = buildAudio(); + return { + audio, + analyzer: new FFTAnalyzer(audio, audioCtx, volume), + }; +}; + +const ControlledMicAnalyzer = ({ + audio, + analyzer, }: { - mode: "AUDIO" | "AUDIO_SCOPE"; - audioSource: "SOUNDCLOUD" | "FILE_UPLOAD"; + audio: HTMLAudioElement; + analyzer: TAnalyzerInputControl; }) => { - const audioCtx = useMemo(() => buildAudioContext(), []); - const audio = useMemo(() => buildAudio(), []); - const analyzer = useMemo(() => { - console.log("Creating analyzer..."); - switch (mode) { - case APPLICATION_MODE.AUDIO: - return new FFTAnalyzer(audio, audioCtx, 1.0); - case APPLICATION_MODE.AUDIO_SCOPE: - return new ScopeAnalyzer(audio, audioCtx); - default: - return mode satisfies never; - } - }, [mode, audio, audioCtx]); + const { onDisabled, onStreamCreated } = useMediaStreamLink(audio, analyzer); + return ( + + ); +}; +const ControlledScreenShareAnalyzer = ({ + audio, + analyzer, +}: { + audio: HTMLAudioElement; + analyzer: TAnalyzerInputControl; +}) => { + const { onDisabled, onStreamCreated } = useMediaStreamLink(audio, analyzer); return ( - <> - - {analyzer instanceof FFTAnalyzer ? ( - - ) : analyzer instanceof ScopeAnalyzer ? ( - - ) : ( - (analyzer satisfies never) - )} - + ); }; -const InternalMediaStreamAnalyzer = ({ +const isMediaStream = (source: TAudioSource) => { + switch (source) { + case AUDIO_SOURCE.MICROPHONE: + case AUDIO_SOURCE.SCREEN_SHARE: + return true; + case AUDIO_SOURCE.SOUNDCLOUD: + case AUDIO_SOURCE.FILE_UPLOAD: + return false; + default: + return source satisfies never; + } +}; + +const ControlledAnalyzer = ({ mode, audioSource, }: { - mode: "AUDIO" | "AUDIO_SCOPE"; - audioSource: "MICROPHONE" | "SCREEN_SHARE"; + mode: typeof APPLICATION_MODE.AUDIO | typeof APPLICATION_MODE.AUDIO_SCOPE; + audioSource: TAudioSource; }) => { - const audioCtx = useMemo(() => buildAudioContext(), []); - const audio = useMemo(() => buildAudio(), []); - const analyzer = useMemo(() => { - console.log("Creating analyzer..."); + const { audio, analyzer } = useMemo(() => { switch (mode) { case APPLICATION_MODE.AUDIO: - return new FFTAnalyzer(audio, audioCtx, 0.0); + return buildFFTAnalyzer(isMediaStream(audioSource) ? 0.0 : 1.0); case APPLICATION_MODE.AUDIO_SCOPE: - return new ScopeAnalyzer(audio, audioCtx); + return buildScopeAnalyzer(); default: return mode satisfies never; } - }, [audio, audioCtx, mode]); - - const { onDisabled, onStreamCreated } = useMediaStreamLink(audio, analyzer); + }, [mode, audioSource]); return ( <> {audioSource === AUDIO_SOURCE.MICROPHONE ? ( - + ) : audioSource === AUDIO_SOURCE.SCREEN_SHARE ? ( - + + ) : audioSource === AUDIO_SOURCE.SOUNDCLOUD || + audioSource === AUDIO_SOURCE.FILE_UPLOAD ? ( + ) : ( (audioSource satisfies never) )} @@ -102,21 +126,14 @@ const InternalMediaStreamAnalyzer = ({ ); }; -const AudioAnalyzer = ({ mode }: { mode: "AUDIO" | "AUDIO_SCOPE" }) => { - const { audioSource } = useAudioSourceContext(); +const AudioAnalyzer = ({ + mode, +}: { + mode: typeof APPLICATION_MODE.AUDIO | typeof APPLICATION_MODE.AUDIO_SCOPE; +}) => { + const { source } = useAudio(); - switch (audioSource) { - case AUDIO_SOURCE.SOUNDCLOUD: - case AUDIO_SOURCE.FILE_UPLOAD: - return ; - case AUDIO_SOURCE.MICROPHONE: - case AUDIO_SOURCE.SCREEN_SHARE: - return ( - - ); - default: - return audioSource satisfies never; - } + return ; }; export default AudioAnalyzer; diff --git a/app/src/components/analyzers/fftAnalyzerControls.tsx b/app/src/components/analyzers/fftAnalyzerControls.tsx index c9fb1d34..8006906f 100644 --- a/app/src/components/analyzers/fftAnalyzerControls.tsx +++ b/app/src/components/analyzers/fftAnalyzerControls.tsx @@ -1,21 +1,18 @@ import { useCallback, useEffect, useRef } from "react"; -import { useFFTAnalyzerContext } from "@/context/fftAnalyzer"; import type FFTAnalyzer from "@/lib/analyzers/fft"; -import { - useAppStateActions, - useEnergyInfo, - useVisualSourceDataX, -} from "@/lib/appState"; +import { useAnalyzerFFT, useMappers } from "@/lib/appState"; +import { COORDINATE_MAPPER_REGISTRY } from "@/lib/mappers/coordinateMappers/registry"; export const FFTAnalyzerControls = ({ analyzer, }: { analyzer: FFTAnalyzer; }) => { - const { octaveBandMode, energyMeasure } = useFFTAnalyzerContext(); - const freqData = useVisualSourceDataX(); - const energyInfo = useEnergyInfo(); - const { resizeVisualSourceData } = useAppStateActions(); + const { octaveBandMode, energyMeasure } = useAnalyzerFFT(); + const { energyTracker } = useMappers(); + const coordinateMapperData = + COORDINATE_MAPPER_REGISTRY.data.hooks.useInstance(); + const { setParams } = COORDINATE_MAPPER_REGISTRY.data.hooks.useActions(); const animationRequestRef = useRef(null!); /** @@ -24,18 +21,18 @@ export const FFTAnalyzerControls = ({ const mapData = useCallback(() => { const bars = analyzer.getBars(); - if (freqData.length != bars.length) { + if (coordinateMapperData.data.length != bars.length) { console.log(`Resizing ${bars.length}`); - resizeVisualSourceData(bars.length); + setParams({ size: bars.length }); return; } - energyInfo.current = analyzer.getEnergy(energyMeasure); + energyTracker?.set(analyzer.getEnergy(energyMeasure)); bars.forEach(({ value }, index) => { - freqData[index] = value; + coordinateMapperData.data[index] = value; }); - }, [freqData, analyzer, resizeVisualSourceData, energyInfo, energyMeasure]); + }, [coordinateMapperData, analyzer, energyTracker, energyMeasure, setParams]); /** * Re-Synchronize the animation loop if the target data destination changes. @@ -50,7 +47,7 @@ export const FFTAnalyzerControls = ({ }; animationRequestRef.current = requestAnimationFrame(animate); return () => cancelAnimationFrame(animationRequestRef.current); - }, [freqData, energyMeasure, mapData]); + }, [coordinateMapperData, energyMeasure, mapData]); /** * Make sure an analyzer exists with the correct mode diff --git a/app/src/components/analyzers/scopeAnalyzerControls.tsx b/app/src/components/analyzers/scopeAnalyzerControls.tsx index 0e4216c3..a8c5d998 100644 --- a/app/src/components/analyzers/scopeAnalyzerControls.tsx +++ b/app/src/components/analyzers/scopeAnalyzerControls.tsx @@ -1,30 +1,31 @@ import { useCallback, useEffect, useRef } from "react"; import type ScopeAnalyzer from "@/lib/analyzers/scope"; -import { - useAppStateActions, - useVisualSourceDataX, - useVisualSourceDataY, -} from "@/lib/appState"; +import { useAppStateActions, useMappers } from "@/lib/appState"; export const AudioScopeAnalyzerControls = ({ analyzer, }: { analyzer: ScopeAnalyzer; }) => { - const timeData = useVisualSourceDataX(); - const quadData = useVisualSourceDataY(); - const { resizeVisualSourceData } = useAppStateActions(); + const { textureMapper } = useMappers(); + const { setMappers } = useAppStateActions(); const animationRequestRef = useRef(null!); /** * Transfers data from the analyzer to the target arrays */ const mapData = useCallback(() => { + const timeData = textureMapper.samplesX; + const quadData = textureMapper.samplesY; // Check if the state sizes need to be updated const targetLength = analyzer.quadSamples.length; if (timeData.length !== targetLength || quadData.length !== targetLength) { console.log(`Resizing ${targetLength}`); - resizeVisualSourceData(targetLength); + setMappers({ + textureMapper: textureMapper.clone({ + size: targetLength, + }), + }); return; } // Copy the data over to state @@ -34,7 +35,7 @@ export const AudioScopeAnalyzerControls = ({ analyzer.quadSamples.forEach((v, index) => { quadData[index] = v; }); - }, [timeData, quadData, analyzer, resizeVisualSourceData]); + }, [analyzer, setMappers, textureMapper]); /** * Re-Synchronize the animation loop if the target data destination changes. @@ -49,7 +50,7 @@ export const AudioScopeAnalyzerControls = ({ }; animationRequestRef.current = requestAnimationFrame(animate); return () => cancelAnimationFrame(animationRequestRef.current); - }, [timeData, quadData, mapData]); + }, [textureMapper, mapData]); return <>; }; diff --git a/app/src/components/audio/sourceControls/common.ts b/app/src/components/audio/sourceControls/common.ts index bc28b72e..b14e6dba 100644 --- a/app/src/components/audio/sourceControls/common.ts +++ b/app/src/components/audio/sourceControls/common.ts @@ -10,7 +10,7 @@ export const AUDIO_SOURCE = { } as const; type ObjectValues = T[keyof T]; -export type AudioSource = ObjectValues; +export type TAudioSource = ObjectValues; export const iOS = (): boolean => { // apple "iP..." device detection. Ex: iPad, iPod, iPhone etc. @@ -24,7 +24,7 @@ export const iOS = (): boolean => { ); }; -export const getPlatformSupportedAudioSources = (): AudioSource[] => { +export const getPlatformSupportedAudioSources = (): TAudioSource[] => { return [ AUDIO_SOURCE.SOUNDCLOUD, AUDIO_SOURCE.MICROPHONE, diff --git a/app/src/components/canvas/AudioScope.tsx b/app/src/components/canvas/AudioScope.tsx index 32e64982..5405a582 100644 --- a/app/src/components/canvas/AudioScope.tsx +++ b/app/src/components/canvas/AudioScope.tsx @@ -1,12 +1,12 @@ -import { CanvasBackground } from "@/components/canvas/common"; -import AudioScopeVisual from "@/components/visualizers/visualizerAudioScope"; import { Canvas } from "@react-three/fiber"; +import ModalVisual from "../visualizers/visualizerModal"; + const AudioScopeCanvas = () => { return ( - - + ; + ); }; diff --git a/app/src/components/canvas/AutoOrbitCamera.tsx b/app/src/components/canvas/AutoOrbitCamera.tsx index e5764327..34275682 100644 --- a/app/src/components/canvas/AutoOrbitCamera.tsx +++ b/app/src/components/canvas/AutoOrbitCamera.tsx @@ -1,9 +1,7 @@ -import { useVisualContext } from "@/context/visual"; +import { useVisual } from "@/lib/appState"; import { useFrame, useThree } from "@react-three/fiber"; import { Spherical, type Vector3 } from "three"; -import { VISUAL } from "../visualizers/common"; - const setFromSphericalZUp = (vec: Vector3, s: Spherical) => { const sinPhiRadius = Math.sin(s.phi) * s.radius; vec.x = sinPhiRadius * Math.sin(s.theta); @@ -13,12 +11,12 @@ const setFromSphericalZUp = (vec: Vector3, s: Spherical) => { }; const useSphericalLimits = () => { - const { visual } = useVisualContext(); + const visual = useVisual(); // r is the Radius // theta is the equator angle // phi is the polar angle - switch (visual) { - case VISUAL.RIBBONS: + switch (visual.id) { + case "ribbons": return { rMin: 10, rMax: 15, @@ -30,7 +28,7 @@ const useSphericalLimits = () => { phiMax: Math.PI / 2.1, phiSpeed: 0.25, }; - case VISUAL.SPHERE: + case "sphere": return { rMin: 10, rMax: 15, @@ -42,7 +40,7 @@ const useSphericalLimits = () => { phiMax: Math.PI / 2, phiSpeed: 0.25, }; - case VISUAL.CUBE: + case "cube": return { rMin: 12, rMax: 20, @@ -54,7 +52,7 @@ const useSphericalLimits = () => { phiMax: Math.PI / 2, phiSpeed: 0.25, }; - case VISUAL.DIFFUSED_RING: + case "diffusedRing": return { rMin: 10, rMax: 18, @@ -66,7 +64,7 @@ const useSphericalLimits = () => { phiMax: Math.PI / 2.25, phiSpeed: 0.25, }; - case VISUAL.WALK: + case "treadmill": return { rMin: 15, rMax: 22, @@ -78,9 +76,9 @@ const useSphericalLimits = () => { phiMax: Math.PI / 2.25, phiSpeed: 0.25, }; - case VISUAL.BOXES: - case VISUAL.DNA: - case VISUAL.GRID: + case "movingBoxes": + case "dna": + case "grid": return { rMin: 15, rMax: 22, @@ -92,6 +90,20 @@ const useSphericalLimits = () => { phiMax: Math.PI / 2, phiSpeed: 0.25, }; + case "swarm": + return { + rMin: 10, + rMax: 15, + rSpeed: 0.1, + thetaMin: 0, + thetaMax: 2 * Math.PI, + thetaSpeed: 0.025, + phiMin: Math.PI / 3, + phiMax: Math.PI / 2, + phiSpeed: 0.25, + }; + case "scope": + return null; default: return visual satisfies never; } @@ -102,20 +114,24 @@ export const AutoOrbitCameraControls = () => { // r is the Radius // theta is the equator angle // phi is the polar angle - const { - rMin, - rMax, - rSpeed, - thetaMin, - thetaMax, - thetaSpeed, - phiMin, - phiMax, - phiSpeed, - } = useSphericalLimits(); + const limits = useSphericalLimits(); const target = new Spherical(); useFrame(({ clock }) => { + if (!limits) { + return; + } + const { + rMin, + rMax, + rSpeed, + thetaMin, + thetaMax, + thetaSpeed, + phiMin, + phiMax, + phiSpeed, + } = limits; const t = clock.elapsedTime; const rAlpha = 0.5 * (1 + Math.sin(t * rSpeed)); diff --git a/app/src/components/canvas/Visual3D.tsx b/app/src/components/canvas/Visual3D.tsx index 610bd2ef..30c4a217 100644 --- a/app/src/components/canvas/Visual3D.tsx +++ b/app/src/components/canvas/Visual3D.tsx @@ -1,77 +1,43 @@ import { BackgroundFog, CanvasBackground } from "@/components/canvas/common"; -import AudioVisual from "@/components/visualizers/visualizerAudio"; -import NoiseVisual from "@/components/visualizers/visualizerNoise"; -import ParticleNoiseVisual from "@/components/visualizers/visualizerParticleNoise"; -import WaveformVisual from "@/components/visualizers/visualizerWaveform"; -import { - CAMERA_CONTROLS_MODE, - useCameraControlsContext, - useCameraControlsContextSetters, -} from "@/context/cameraControls"; -import { useVisualContext } from "@/context/visual"; -import { APPLICATION_MODE } from "@/lib/applicationModes"; -import { useUser } from "@/lib/appState"; +import ModalVisual from "@/components/visualizers/visualizerModal"; +import { useAppStateActions, useCameraState, useUser } from "@/lib/appState"; import { OrbitControls } from "@react-three/drei"; import { Canvas, useFrame } from "@react-three/fiber"; import { AutoOrbitCameraControls } from "./AutoOrbitCamera"; import { PaletteTracker } from "./paletteTracker"; -const VisualizerComponent = ({ - mode, -}: { - mode: "WAVE_FORM" | "NOISE" | "AUDIO" | "PARTICLE_NOISE"; -}) => { - const { visual } = useVisualContext(); - switch (mode) { - case APPLICATION_MODE.WAVE_FORM: - return ; - case APPLICATION_MODE.NOISE: - return ; - case APPLICATION_MODE.PARTICLE_NOISE: - return ; - case APPLICATION_MODE.AUDIO: - return ; - default: - return mode satisfies never; - } -}; - const CameraControls = () => { - const { mode, autoOrbitAfterSleepMs } = useCameraControlsContext(); - const { setMode } = useCameraControlsContextSetters(); + const { mode, autoOrbitAfterSleepMs } = useCameraState(); + const { setCamera } = useAppStateActions(); const { canvasInteractionEventTracker } = useUser(); useFrame(() => { if ( - mode === CAMERA_CONTROLS_MODE.ORBIT_CONTROLS && + mode === "ORBIT_CONTROLS" && autoOrbitAfterSleepMs > 0 && canvasInteractionEventTracker.msSinceLastEvent > autoOrbitAfterSleepMs ) { - setMode(CAMERA_CONTROLS_MODE.AUTO_ORBIT); + setCamera({ mode: "AUTO_ORBIT" }); } else if ( - mode === CAMERA_CONTROLS_MODE.AUTO_ORBIT && + mode === "AUTO_ORBIT" && canvasInteractionEventTracker.msSinceLastEvent < autoOrbitAfterSleepMs ) { - setMode(CAMERA_CONTROLS_MODE.ORBIT_CONTROLS); + setCamera({ mode: "ORBIT_CONTROLS" }); } }); switch (mode) { - case CAMERA_CONTROLS_MODE.ORBIT_CONTROLS: + case "ORBIT_CONTROLS": return ; - case CAMERA_CONTROLS_MODE.AUTO_ORBIT: + case "AUTO_ORBIT": return ; default: return mode satisfies never; } }; -const Visual3DCanvas = ({ - mode, -}: { - mode: "WAVE_FORM" | "NOISE" | "AUDIO" | "PARTICLE_NOISE"; -}) => { +const Visual3DCanvas = () => { return ( - + {/* */} diff --git a/app/src/components/canvas/common.tsx b/app/src/components/canvas/common.tsx index 10d85ee9..71f0c281 100644 --- a/app/src/components/canvas/common.tsx +++ b/app/src/components/canvas/common.tsx @@ -1,10 +1,8 @@ -import { useVisualContext } from "@/context/visual"; -import { usePalette } from "@/lib/appState"; +import { useAppearance } from "@/lib/appState"; import { ColorPalette } from "@/lib/palettes"; const useBackgroundColor = () => { - const { colorBackground } = useVisualContext(); - const palette = usePalette(); + const { colorBackground, palette } = useAppearance(); return colorBackground ? ColorPalette.getPalette(palette).calcBackgroundColor(0) : "#010204"; diff --git a/app/src/components/canvas/paletteTracker.tsx b/app/src/components/canvas/paletteTracker.tsx index 92bb7302..cd8316fe 100644 --- a/app/src/components/canvas/paletteTracker.tsx +++ b/app/src/components/canvas/paletteTracker.tsx @@ -1,8 +1,6 @@ -import { useVisualContext } from "@/context/visual"; import { ScalarMovingAvgEventDetector } from "@/lib/analyzers/scalarEventDetector"; -import { useAppStateActions, useEnergyInfo } from "@/lib/appState"; +import { useAppearance, useAppStateActions, useMappers } from "@/lib/appState"; import { type IScalarTracker } from "@/lib/mappers/valueTracker/common"; -import { EnergyTracker } from "@/lib/mappers/valueTracker/energyTracker"; import { useFrame } from "@react-three/fiber"; const PaletteUpdater = ({ @@ -10,7 +8,7 @@ const PaletteUpdater = ({ }: { scalarTracker: IScalarTracker; }) => { - const { paletteTrackEnergy: enabled } = useVisualContext(); + const { paletteTrackEnergy: enabled } = useAppearance(); const detector = new ScalarMovingAvgEventDetector(0.5, 50, 500); const { nextPalette } = useAppStateActions(); @@ -19,7 +17,7 @@ const PaletteUpdater = ({ return; } - if (detector.step(scalarTracker.getNormalizedValue())) { + if (detector.step(scalarTracker.get())) { nextPalette(); } }); @@ -28,8 +26,8 @@ const PaletteUpdater = ({ }; export const PaletteTracker = () => { - const energyInfo = useEnergyInfo(); - const scalarTracker = new EnergyTracker(energyInfo); - - return ; + const { energyTracker } = useMappers(); + return energyTracker ? ( + + ) : null; }; diff --git a/app/src/components/controls/audioSource/soundcloud/player.tsx b/app/src/components/controls/audioSource/soundcloud/player.tsx index 58e1de67..ce15262e 100644 --- a/app/src/components/controls/audioSource/soundcloud/player.tsx +++ b/app/src/components/controls/audioSource/soundcloud/player.tsx @@ -4,8 +4,8 @@ import { type ComponentPropsWithoutRef, type HTMLAttributes, } from "react"; -import { useModeContext } from "@/context/mode"; import { useSoundcloudContext } from "@/context/soundcloud"; +import { useAppearance } from "@/lib/appState"; import { getTrackStreamUrl } from "@/lib/soundcloud/api"; import { type SoundcloudTrack } from "@/lib/soundcloud/models"; import { cn } from "@/lib/utils"; @@ -21,7 +21,7 @@ export const TrackPlayer = ({ audio: HTMLAudioElement; track: SoundcloudTrack; }) => { - const { showUI } = useModeContext(); + const { showUI } = useAppearance(); const { data: streamUrl } = useSuspenseQuery({ queryKey: ["soundcloud-stream-url", track.id], diff --git a/app/src/components/controls/common.tsx b/app/src/components/controls/common.tsx index d8a0da7d..3238f820 100644 --- a/app/src/components/controls/common.tsx +++ b/app/src/components/controls/common.tsx @@ -1,4 +1,5 @@ -import { type HTMLAttributes, type ReactNode } from "react"; +import { type HTMLAttributes, type HTMLProps, type ReactNode } from "react"; +import { Label } from "@/components/ui/label"; import { Popover, PopoverContent, @@ -6,6 +7,28 @@ import { } from "@/components/ui/popover"; import { cn } from "@/lib/utils"; +export const ValueLabel = ({ + label, + value, + className, + ...props +}: HTMLProps & { + label: string; + value: string | number; +}) => { + return ( +
+ + + {value} + +
+ ); +}; + export const ToolbarItem = ({ children, className, diff --git a/app/src/components/controls/dock.tsx b/app/src/components/controls/dock.tsx index ebdf6fd0..b7bfc825 100644 --- a/app/src/components/controls/dock.tsx +++ b/app/src/components/controls/dock.tsx @@ -1,62 +1,84 @@ -import { forwardRef, type HTMLAttributes, type HTMLProps } from "react"; +import { useMemo, useState } from "react"; +import { VISUAL_REGISTRY } from "@/components/visualizers/registry"; +import { useAppStateActions, useMode, useVisual } from "@/lib/appState"; import { cn } from "@/lib/utils"; +import { Palette, Settings } from "lucide-react"; -export const DockItem = forwardRef>( - ({ className, ...props }, ref) => { - return ( -
- ); - }, -); -DockItem.displayName = "DockItem"; +import { Dock, DockCard } from "../ui/dock"; +import { Sheet, SheetContent } from "../ui/sheet"; +import { ModeSheetContent } from "./modeSheet"; +import { VisualSettingsSheetContent } from "./visualSettingsSheet"; + +export const SettingsDockCard = () => { + const [open, setOpen] = useState(false); + return ( + <> + setOpen((prev) => !prev)} + > + + + + + + + + + + ); +}; -export const DockNav = ({ - className, - children, - ...props -}: HTMLAttributes) => { +export const VisualSettingsSheetDockCard = () => { + const [open, setOpen] = useState(false); return ( -
- {children} -
+ <> + setOpen((prev) => !prev)} + > + + + + + + ); }; -export const Dock = ({ - className, - children, - ...props -}: HTMLAttributes) => { +export const ApplicationDock = () => { + const activeVisual = useVisual(); + const { setVisual } = useAppStateActions(); + const mode = useMode(); + + const supportedVisuals = useMemo(() => { + return Object.values(VISUAL_REGISTRY).filter((visual) => + [...visual.supportedApplicationModes].includes(mode), + ); + }, [mode]); return ( -
- {children} -
+ }> + {supportedVisuals.map((visual) => ( + setVisual(visual.id)} + active={activeVisual.id === visual.id} + className={cn({ + "from-slate-500": activeVisual.id === visual.id, + })} + > + + + ))} + ); }; + +export default ApplicationDock; diff --git a/app/src/components/controls/main.tsx b/app/src/components/controls/main.tsx index bb6880b9..83ee0de4 100644 --- a/app/src/components/controls/main.tsx +++ b/app/src/components/controls/main.tsx @@ -1,14 +1,10 @@ -import VisualsDock from "@/components/controls/visualsDock"; +import VisualsDock from "@/components/controls/dock"; import { Switch } from "@/components/ui/switch"; -import { useModeContext, useModeContextSetters } from "@/context/mode"; -import { APPLICATION_MODE } from "@/lib/applicationModes"; -import { cn } from "@/lib/utils"; - -import SettingsDock from "./settingsDock"; +import { useAppearance, useAppStateActions } from "@/lib/appState"; export const ControlsPanel = () => { - const { mode, showUI } = useModeContext(); - const { setShowUI } = useModeContextSetters(); + const { showUI } = useAppearance(); + const { setAppearance } = useAppStateActions(); return ( <>
@@ -17,24 +13,11 @@ export const ControlsPanel = () => { className="pointer-events-auto cursor-pointer" id="controls-visible" onCheckedChange={(e) => { - setShowUI(e); + setAppearance({ showUI: e }); }} />
- {showUI && ( -
- {mode !== APPLICATION_MODE.AUDIO_SCOPE && ( - - )} -
- -
-
- )} + {showUI && } ); }; diff --git a/app/src/components/controls/mode/audio.tsx b/app/src/components/controls/mode/audio.tsx index 847dd17e..4bdcf1e8 100644 --- a/app/src/components/controls/mode/audio.tsx +++ b/app/src/components/controls/mode/audio.tsx @@ -7,41 +7,46 @@ import { } from "@/components/ui/select"; import { Slider } from "@/components/ui/slider"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { - useFFTAnalyzerContext, - useFFTAnalyzerContextSetters, -} from "@/context/fftAnalyzer"; import { EnergyMeasureOptions, OctaveBandModeMap, type EnergyMeasure, type OctaveBandMode, } from "@/lib/analyzers/fft"; +import { useAnalyzerFFT, useAppStateActions } from "@/lib/appState"; +import { COORDINATE_MAPPER_REGISTRY } from "@/lib/mappers/coordinateMappers/registry"; -import { AudioSourceControls, AudioSourceSelect, ValueLabel } from "./common"; +import { ValueLabel } from "../common"; +import { AudioSourceControls, AudioSourceSelect } from "./common"; const FFTAnalyzerControls = () => { - const { amplitude, octaveBandMode, energyMeasure } = useFFTAnalyzerContext(); - const { setAmplitude, setOctaveBand, setEnergyMeasure } = - useFFTAnalyzerContextSetters(); + const { octaveBandMode, energyMeasure } = useAnalyzerFFT(); + const { setAnalyzerFFT } = useAppStateActions(); + const mapper = COORDINATE_MAPPER_REGISTRY.data.hooks.useInstance(); + const { setParams } = COORDINATE_MAPPER_REGISTRY.data.hooks.useActions(); return (
- + setAmplitude(e[0])} + onValueChange={(e) => setParams({ amplitude: e[0] })} />
Octave Band Mode { - setEnergyMeasure(v as EnergyMeasure); + setAnalyzerFFT({ + energyMeasure: v as EnergyMeasure, + }); }} > diff --git a/app/src/components/controls/mode/common.tsx b/app/src/components/controls/mode/common.tsx index 62c4cebe..70a5b0dd 100644 --- a/app/src/components/controls/mode/common.tsx +++ b/app/src/components/controls/mode/common.tsx @@ -1,16 +1,12 @@ -import { useMemo, type HTMLAttributes, type HTMLProps } from "react"; +import { useMemo, type HTMLAttributes } from "react"; import { AUDIO_SOURCE, getPlatformSupportedAudioSources, - type AudioSource, + type TAudioSource, } from "@/components/audio/sourceControls/common"; import { FileUploadControls } from "@/components/controls/audioSource/fileUpload"; import { SoundcloudControls } from "@/components/controls/audioSource/soundcloud/controls"; -import { Label } from "@/components/ui/label"; -import { - useAudioSourceContext, - useAudioSourceContextSetters, -} from "@/context/audioSource"; +import { useAppStateActions, useAudio } from "@/lib/appState"; import { cn } from "@/lib/utils"; import { FileUp, @@ -20,32 +16,10 @@ import { type LucideProps, } from "lucide-react"; -export const ValueLabel = ({ - label, - value, - className, - ...props -}: HTMLProps & { - label: string; - value: string | number; -}) => { - return ( -
- - - {value} - -
- ); -}; - const AudioSourceIcon = ({ audioSource, ...props -}: { audioSource: AudioSource } & LucideProps) => { +}: { audioSource: TAudioSource } & LucideProps) => { switch (audioSource) { case AUDIO_SOURCE.SOUNDCLOUD: return ; @@ -80,8 +54,8 @@ export const AudioSourceSelect = ({ className, ...props }: HTMLAttributes) => { - const { audioSource } = useAudioSourceContext(); - const { setAudioSource } = useAudioSourceContextSetters(); + const { source: activeSource } = useAudio(); + const { setAudio } = useAppStateActions(); const available = useMemo(() => { return getPlatformSupportedAudioSources(); }, []); @@ -97,8 +71,8 @@ export const AudioSourceSelect = ({ {available.map((source) => ( setAudioSource(source)} - aria-selected={audioSource === source} + onClick={() => setAudio({ source })} + aria-selected={activeSource === source} > @@ -108,8 +82,8 @@ export const AudioSourceSelect = ({ }; export const AudioSourceControls = () => { - const { audioSource } = useAudioSourceContext(); - switch (audioSource) { + const { source } = useAudio(); + switch (source) { case AUDIO_SOURCE.SOUNDCLOUD: return ; case AUDIO_SOURCE.FILE_UPLOAD: @@ -119,6 +93,6 @@ export const AudioSourceControls = () => { // TODO: Add controls return null; default: - return audioSource satisfies never; + return source satisfies never; } }; diff --git a/app/src/components/controls/mode/noise.tsx b/app/src/components/controls/mode/noise.tsx deleted file mode 100644 index 5184346a..00000000 --- a/app/src/components/controls/mode/noise.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Slider } from "@/components/ui/slider"; -import { - useNoiseGeneratorContext, - useNoiseGeneratorContextSetters, -} from "@/context/noiseGenerator"; -import { RefreshCcw } from "lucide-react"; - -import { ValueLabel } from "./common"; - -export const NoiseGeneratorModeControls = () => { - const { amplitude, spatialScale, timeScale, nIterations } = - useNoiseGeneratorContext(); - const { setAmplitude, setSpatialScale, setTimeScale, setNIterations, reset } = - useNoiseGeneratorContextSetters(); - return ( -
-
- Noise - reset()} - /> -
- - setAmplitude(e[0])} - /> - - setSpatialScale(e[0])} - /> - - setTimeScale(e[0])} - /> - - setNIterations(e[0])} - /> -
- ); -}; diff --git a/app/src/components/controls/mode/waveform.tsx b/app/src/components/controls/mode/waveform.tsx deleted file mode 100644 index a5954a7c..00000000 --- a/app/src/components/controls/mode/waveform.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { Label } from "@/components/ui/label"; -import { Slider } from "@/components/ui/slider"; -import { Switch } from "@/components/ui/switch"; -import { - useWaveGeneratorContext, - useWaveGeneratorContextSetters, -} from "@/context/waveGenerator"; -import { RefreshCcw } from "lucide-react"; - -import { ValueLabel } from "./common"; - -export const WaveformModeControls = () => { - const { maxAmplitude, waveformFrequenciesHz } = useWaveGeneratorContext(); - const { setMaxAmplitude, setWaveformFrequenciesHz, reset } = - useWaveGeneratorContextSetters(); - - return ( -
-
- Wave Form - reset()} - /> -
-
- - 1} - defaultChecked={waveformFrequenciesHz.length > 1} - onCheckedChange={(e) => { - setWaveformFrequenciesHz(e ? [2.0, 10.0] : [2.0]); - }} - /> -
- - setMaxAmplitude(e[0])} - /> - {[...waveformFrequenciesHz].map((hz, i) => ( -
- - - setWaveformFrequenciesHz( - (prev) => - prev.map((v, j) => (i == j ? e[0] : v)) as [ - number, - ...number[], - ], - ) - } - /> -
- ))} -
- ); -}; diff --git a/app/src/components/controls/modeSheet.tsx b/app/src/components/controls/modeSheet.tsx index fb29cfce..89f7cd23 100644 --- a/app/src/components/controls/modeSheet.tsx +++ b/app/src/components/controls/modeSheet.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState, type PropsWithChildren } from "react"; +import { useMemo } from "react"; import { Select, SelectContent, @@ -7,46 +7,36 @@ import { SelectValue, } from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; -import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; -import { useModeContext, useModeContextSetters } from "@/context/mode"; import { APPLICATION_MODE, getPlatformSupportedApplicationModes, isAudioMode, - type ApplicationMode, + type TApplicationMode, } from "@/lib/applicationModes"; -import { - AudioWaveform, - Drum, - HelpCircle, - Music, - Shell, - Waves, -} from "lucide-react"; +import { useAppStateActions, useMode } from "@/lib/appState"; +import { COORDINATE_MAPPER_REGISTRY } from "@/lib/mappers/coordinateMappers/registry"; +import { AudioWaveform, Music, Shell, Waves, Wind } from "lucide-react"; import { AudioModeControls } from "./mode/audio"; import { AudioScopeModeControls } from "./mode/audioScope"; -import { NoiseGeneratorModeControls } from "./mode/noise"; -import { WaveformModeControls } from "./mode/waveform"; -const ModeIcon = ({ mode }: { mode: ApplicationMode }) => { +const ModeIcon = ({ mode }: { mode: TApplicationMode }) => { switch (mode) { - case "WAVE_FORM": + case APPLICATION_MODE.WAVE_FORM: return ; - case "NOISE": + case APPLICATION_MODE.NOISE: return ; - case "AUDIO": + case APPLICATION_MODE.AUDIO: return ; - case "AUDIO_SCOPE": + case APPLICATION_MODE.AUDIO_SCOPE: return ; - case "PARTICLE_NOISE": - return ; + case APPLICATION_MODE.PARTICLE_NOISE: + return ; default: - return ; - // return mode satisfies never; + return mode satisfies never; } }; -const ModeSelectEntry = ({ mode }: { mode: ApplicationMode }) => { +const ModeSelectEntry = ({ mode }: { mode: TApplicationMode }) => { return (
{isAudioMode(mode) && "🎧"}
@@ -57,8 +47,8 @@ const ModeSelectEntry = ({ mode }: { mode: ApplicationMode }) => { }; const ModeSelector = () => { - const { mode } = useModeContext(); - const { setMode } = useModeContextSetters(); + const mode = useMode(); + const { setMode } = useAppStateActions(); const availableModes = useMemo(() => { return getPlatformSupportedApplicationModes(); @@ -67,7 +57,7 @@ const ModeSelector = () => { return (