diff --git a/README.md b/README.md index 0ae604da..b3b4cd67 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ An interactive audio visualizer built with react and THREE.js. --- -[![docs/waveform.gif](docs/waveform.gif)](https://dcyoung.github.io/r3f-audio-visualizer/) +[![docs/demo-2024-1-12.gif](docs/demo-2024-1-12.gif)](https://dcyoung.github.io/r3f-audio-visualizer/) --- diff --git a/app/src/App.tsx b/app/src/App.tsx index 808e08ff..89ab6b51 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -6,6 +6,8 @@ import { ControlsPanel } from "@/components/controls/main"; import { useModeContext } from "@/context/mode"; import { APPLICATION_MODE, type ApplicationMode } from "@/lib/applicationModes"; +import { useAppStateActions } from "./lib/appState"; + const getAnalyzerComponent = (mode: ApplicationMode) => { switch (mode) { case APPLICATION_MODE.AUDIO: @@ -36,9 +38,15 @@ const getCanvasComponent = (mode: ApplicationMode) => { const App = () => { const { mode } = useModeContext(); + const { noteCanvasInteraction } = useAppStateActions(); + return (
-
+
loading...}> {getCanvasComponent(mode)} diff --git a/app/src/components/controls/settingsDock.tsx b/app/src/components/controls/settingsDock.tsx index 707f7586..436cd4e2 100644 --- a/app/src/components/controls/settingsDock.tsx +++ b/app/src/components/controls/settingsDock.tsx @@ -1,54 +1,15 @@ import { type HTMLAttributes } from "react"; -import { - CAMERA_CONTROLS_MODE, - useCameraControlsContext, - useCameraControlsContextSetters, -} from "@/context/cameraControls"; -import { useModeContext } from "@/context/mode"; -import { isCameraMode } from "@/lib/applicationModes"; import { cn } from "@/lib/utils"; -import { Grab, Palette, Rotate3d, Settings } from "lucide-react"; +import { Palette, Settings } from "lucide-react"; import { Dock, DockItem, DockNav } from "./dock"; import { ModeSheet } from "./modeSheet"; import { VisualSettingsSheet } from "./visualSettingsSheet"; -const CameraControlsDockItem = () => { - const { mode } = useCameraControlsContext(); - const { setMode } = useCameraControlsContextSetters(); - return ( - { - setMode((prev) => { - switch (prev) { - case CAMERA_CONTROLS_MODE.ORBIT_CONTROLS: - return CAMERA_CONTROLS_MODE.AUTO_ORBIT; - case CAMERA_CONTROLS_MODE.AUTO_ORBIT: - return CAMERA_CONTROLS_MODE.ORBIT_CONTROLS; - default: - return prev satisfies never; - } - }); - }} - > - {mode === CAMERA_CONTROLS_MODE.ORBIT_CONTROLS ? ( - - ) : mode === CAMERA_CONTROLS_MODE.AUTO_ORBIT ? ( - - ) : ( - (mode satisfies never) - )} - - ); -}; - export const SettingsDock = ({ className, ...props }: HTMLAttributes) => { - const { mode } = useModeContext(); - return ( @@ -62,7 +23,6 @@ export const SettingsDock = ({ - {isCameraMode(mode) && } ); diff --git a/app/src/components/controls/visualSettingsSheet.tsx b/app/src/components/controls/visualSettingsSheet.tsx index 7356ba6a..f073fefe 100644 --- a/app/src/components/controls/visualSettingsSheet.tsx +++ b/app/src/components/controls/visualSettingsSheet.tsx @@ -3,9 +3,14 @@ import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; import { Switch } from "@/components/ui/switch"; +import { + CAMERA_CONTROLS_MODE, + useCameraControlsContext, + useCameraControlsContextSetters, +} from "@/context/cameraControls"; import { useModeContext } from "@/context/mode"; import { useVisualContext, useVisualContextSetters } from "@/context/visual"; -import { APPLICATION_MODE } from "@/lib/applicationModes"; +import { APPLICATION_MODE, isCameraMode } from "@/lib/applicationModes"; import { useAppStateActions, usePalette } from "@/lib/appState"; import { AVAILABLE_COLOR_PALETTES, @@ -83,7 +88,9 @@ export const VisualSettingsSheet = ({ children }: PropsWithChildren) => { useVisualContextSetters(); const palette = usePalette(); const { setPalette } = useAppStateActions(); - + const { autoOrbitAfterSleepMs } = useCameraControlsContext(); + const { setMode: setCameraMode, setAutoOrbitAfterSleepMs } = + useCameraControlsContextSetters(); return ( {children} @@ -117,7 +124,7 @@ export const VisualSettingsSheet = ({ children }: PropsWithChildren) => { />
- + { }} />
+
+ + 0} + onCheckedChange={(e) => { + setCameraMode( + e + ? CAMERA_CONTROLS_MODE.AUTO_ORBIT + : CAMERA_CONTROLS_MODE.ORBIT_CONTROLS, + ); + setAutoOrbitAfterSleepMs(e ? 3500 : 0); + }} + /> +
diff --git a/app/src/context/cameraControls.tsx b/app/src/context/cameraControls.tsx index 45f38561..a270e6d7 100644 --- a/app/src/context/cameraControls.tsx +++ b/app/src/context/cameraControls.tsx @@ -1,11 +1,14 @@ import { createContext, useContext, + useEffect, useState, type Dispatch, type PropsWithChildren, type SetStateAction, } from "react"; +import { useTimeout } from "@/hooks/useTimeout"; +import { useLastCanvasInteraction } from "@/lib/appState"; export const CAMERA_CONTROLS_MODE = { AUTO_ORBIT: "AUTO_ORBIT", @@ -16,30 +19,56 @@ export type CameraControlsMode = export interface CameraControlsConfig { mode: CameraControlsMode; + autoOrbitAfterSleepMs: number; // disabled if <= 0 } export const CameraControlsContext = createContext<{ config: CameraControlsConfig; setters: { setMode: Dispatch>; + setAutoOrbitAfterSleepMs: Dispatch>; }; } | null>(null); export const CameraControlsContextProvider = ({ + initial = undefined, children, -}: PropsWithChildren) => { +}: PropsWithChildren & { + initial?: Partial; +}) => { + const lastCanvasInteraction = useLastCanvasInteraction(); const [mode, setMode] = useState( - CAMERA_CONTROLS_MODE.ORBIT_CONTROLS, + initial?.mode ?? CAMERA_CONTROLS_MODE.ORBIT_CONTROLS, ); + const [autoOrbitAfterSleepMs, setAutoOrbitAfterSleepMs] = useState( + initial?.autoOrbitAfterSleepMs ?? 10000, + ); + + const { reset } = useTimeout(autoOrbitAfterSleepMs, () => { + if (mode !== CAMERA_CONTROLS_MODE.AUTO_ORBIT) { + setMode(CAMERA_CONTROLS_MODE.AUTO_ORBIT); + } + }); + + // TODO: Investigate behavior here + useEffect(() => { + // If the user has interacted with the canvas, set the mode back to manual control + if (mode === CAMERA_CONTROLS_MODE.AUTO_ORBIT) { + setMode(CAMERA_CONTROLS_MODE.ORBIT_CONTROLS); + } + reset(); + }, [lastCanvasInteraction, reset]); return ( diff --git a/app/src/hooks/useTimeout.ts b/app/src/hooks/useTimeout.ts new file mode 100644 index 00000000..bb7d2436 --- /dev/null +++ b/app/src/hooks/useTimeout.ts @@ -0,0 +1,49 @@ +import { useCallback, useEffect, useRef } from "react"; + +export const useTimeout = (delayMs: number, onTimerEnd: () => void) => { + const savedCallback = useRef(onTimerEnd); + const handle = useRef(null); + + // Handle changing callback fxn + useEffect(() => { + savedCallback.current = onTimerEnd; + }, [onTimerEnd]); + + // allow reseting the timer + const reset = useCallback(() => { + if (handle.current) { + clearTimeout(handle.current); + } + + if (delayMs <= 0) { + return; + } + + handle.current = setTimeout(() => { + savedCallback.current(); + }, delayMs); + }, []); + + // Initialize and cleanup + useEffect(() => { + if (handle.current) { + clearTimeout(handle.current); + } + + if (delayMs <= 0) { + return; + } + + handle.current = setTimeout(() => { + savedCallback.current(); + }, delayMs); + + return () => { + if (handle.current) { + clearTimeout(handle.current); + } + }; + }, [delayMs]); + + return { reset }; +}; diff --git a/app/src/lib/appState.ts b/app/src/lib/appState.ts index 4061835e..ba8433f9 100644 --- a/app/src/lib/appState.ts +++ b/app/src/lib/appState.ts @@ -7,6 +7,9 @@ import { } from "./palettes"; interface IAppState { + user: { + lastCanvasInteraction: Date; + }; visual: { palette: ColorPaletteType; }; @@ -18,6 +21,7 @@ interface IAppState { current: number; }; actions: { + noteCanvasInteraction: () => void; setPalette: (newPalette: ColorPaletteType) => void; nextPalette: () => void; resizeVisualSourceData: (newSize: number) => void; @@ -25,6 +29,9 @@ interface IAppState { } const useAppState = create((set, _) => ({ + user: { + lastCanvasInteraction: new Date(), + }, visual: { palette: COLOR_PALETTE.THREE_COOL_TO_WARM, }, @@ -34,6 +41,14 @@ const useAppState = create((set, _) => ({ }, energyInfo: { current: 0 }, actions: { + noteCanvasInteraction: () => + set((_) => { + return { + user: { + lastCanvasInteraction: new Date(), + }, + }; + }), setPalette: (newPalette: ColorPaletteType) => set((_) => { return { @@ -61,6 +76,8 @@ const useAppState = create((set, _) => ({ }, })); +export const useLastCanvasInteraction = () => + useAppState((state) => state.user.lastCanvasInteraction); export const usePalette = () => useAppState((state) => state.visual.palette); export const useVisualSourceDataX = () => useAppState((state) => state.visualSourceData.x); diff --git a/docs/demo-2024-1-12.gif b/docs/demo-2024-1-12.gif new file mode 100644 index 00000000..7e44aa5b Binary files /dev/null and b/docs/demo-2024-1-12.gif differ