Skip to content

Commit

Permalink
Add auto orbit after no user interaction
Browse files Browse the repository at this point in the history
  • Loading branch information
dcyoung committed Feb 13, 2024
1 parent 8ac0d17 commit 40bbae1
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 50 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/)

---

Expand Down
10 changes: 9 additions & 1 deletion app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -36,9 +38,15 @@ const getCanvasComponent = (mode: ApplicationMode) => {

const App = () => {
const { mode } = useModeContext();
const { noteCanvasInteraction } = useAppStateActions();

return (
<main className="relative h-[100dvh] w-[100dvw] bg-black">
<div className="absolute h-[100dvh] w-[100dvw]">
<div
className="absolute h-[100dvh] w-[100dvw]"
onMouseDown={noteCanvasInteraction}
onTouchStart={noteCanvasInteraction}
>
<Suspense fallback={<span>loading...</span>}>
{getCanvasComponent(mode)}
</Suspense>
Expand Down
42 changes: 1 addition & 41 deletions app/src/components/controls/settingsDock.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<DockItem
className="rounded-full"
onClick={() => {
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 ? (
<Rotate3d />
) : mode === CAMERA_CONTROLS_MODE.AUTO_ORBIT ? (
<Grab />
) : (
(mode satisfies never)
)}
</DockItem>
);
};

export const SettingsDock = ({
className,
...props
}: HTMLAttributes<HTMLDivElement>) => {
const { mode } = useModeContext();

return (
<Dock {...props} className={cn("max-h-4/5 w-fit sm:h-fit", className)}>
<DockNav className="snap-y flex-col bg-gradient-to-l sm:snap-x sm:flex-row sm:bg-gradient-to-t">
Expand All @@ -62,7 +23,6 @@ export const SettingsDock = ({
<Palette />
</DockItem>
</VisualSettingsSheet>
{isCameraMode(mode) && <CameraControlsDockItem />}
</DockNav>
</Dock>
);
Expand Down
28 changes: 25 additions & 3 deletions app/src/components/controls/visualSettingsSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>{children}</SheetTrigger>
Expand Down Expand Up @@ -117,7 +124,7 @@ export const VisualSettingsSheet = ({ children }: PropsWithChildren) => {
/>
</div>
<div className="flex items-center justify-between gap-2">
<Label>Follow Music</Label>
<Label>Colors Follow Music</Label>
<Switch
disabled={mode !== APPLICATION_MODE.AUDIO}
defaultChecked={paletteTrackEnergy}
Expand All @@ -126,6 +133,21 @@ export const VisualSettingsSheet = ({ children }: PropsWithChildren) => {
}}
/>
</div>
<div className="flex items-center justify-between gap-2">
<Label>Auto Orbit Camera</Label>
<Switch
disabled={!isCameraMode(mode)}
defaultChecked={autoOrbitAfterSleepMs > 0}
onCheckedChange={(e) => {
setCameraMode(
e
? CAMERA_CONTROLS_MODE.AUTO_ORBIT
: CAMERA_CONTROLS_MODE.ORBIT_CONTROLS,
);
setAutoOrbitAfterSleepMs(e ? 3500 : 0);
}}
/>
</div>
</div>
<Separator />
<div className="space-y-4">
Expand Down
37 changes: 33 additions & 4 deletions app/src/context/cameraControls.tsx
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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<SetStateAction<CameraControlsMode>>;
setAutoOrbitAfterSleepMs: Dispatch<SetStateAction<number>>;
};
} | null>(null);

export const CameraControlsContextProvider = ({
initial = undefined,
children,
}: PropsWithChildren) => {
}: PropsWithChildren & {
initial?: Partial<CameraControlsConfig>;
}) => {
const lastCanvasInteraction = useLastCanvasInteraction();
const [mode, setMode] = useState<CameraControlsMode>(
CAMERA_CONTROLS_MODE.ORBIT_CONTROLS,
initial?.mode ?? CAMERA_CONTROLS_MODE.ORBIT_CONTROLS,
);
const [autoOrbitAfterSleepMs, setAutoOrbitAfterSleepMs] = useState<number>(
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 (
<CameraControlsContext.Provider
value={{
config: {
mode: mode,
mode,
autoOrbitAfterSleepMs,
},
setters: {
setMode: setMode,
setMode,
setAutoOrbitAfterSleepMs,
},
}}
>
Expand Down
49 changes: 49 additions & 0 deletions app/src/hooks/useTimeout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useCallback, useEffect, useRef } from "react";

export const useTimeout = (delayMs: number, onTimerEnd: () => void) => {
const savedCallback = useRef(onTimerEnd);
const handle = useRef<NodeJS.Timeout | null>(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 };
};
17 changes: 17 additions & 0 deletions app/src/lib/appState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import {
} from "./palettes";

interface IAppState {
user: {
lastCanvasInteraction: Date;
};
visual: {
palette: ColorPaletteType;
};
Expand All @@ -18,13 +21,17 @@ interface IAppState {
current: number;
};
actions: {
noteCanvasInteraction: () => void;
setPalette: (newPalette: ColorPaletteType) => void;
nextPalette: () => void;
resizeVisualSourceData: (newSize: number) => void;
};
}

const useAppState = create<IAppState>((set, _) => ({
user: {
lastCanvasInteraction: new Date(),
},
visual: {
palette: COLOR_PALETTE.THREE_COOL_TO_WARM,
},
Expand All @@ -34,6 +41,14 @@ const useAppState = create<IAppState>((set, _) => ({
},
energyInfo: { current: 0 },
actions: {
noteCanvasInteraction: () =>
set((_) => {
return {
user: {
lastCanvasInteraction: new Date(),
},
};
}),
setPalette: (newPalette: ColorPaletteType) =>
set((_) => {
return {
Expand Down Expand Up @@ -61,6 +76,8 @@ const useAppState = create<IAppState>((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);
Expand Down
Binary file added docs/demo-2024-1-12.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 40bbae1

Please sign in to comment.