-
-
Palette
-
-
- {AVAILABLE_COLOR_PALETTES.map((p) => (
-
setPalette(p)}
- aria-selected={p === palette}
- />
- ))}
-
-
-
-
- {
- setColorBackground(e);
- }}
- />
-
-
-
-
{
- setPaletteTrackEnergy(e);
- }}
+
+
+
+
Palette
+
+
+ {AVAILABLE_COLOR_PALETTES.map((p) => (
+
setAppearance({ palette: p })}
+ aria-selected={p === palette}
/>
-
-
-
- 0}
- onCheckedChange={(e) => {
- setCameraMode(
- e
- ? CAMERA_CONTROLS_MODE.AUTO_ORBIT
- : CAMERA_CONTROLS_MODE.ORBIT_CONTROLS,
- );
- setAutoOrbitAfterSleepMs(e ? 3500 : 0);
- }}
- />
-
-
-
-
-
+ ))}
-
-
+
+
+ setAppearance({ colorBackground: e })}
+ />
+
+
+
+ setAppearance({ paletteTrackEnergy: e })}
+ />
+
+
+
+ 0}
+ onCheckedChange={(e) => {
+ setCamera(
+ e
+ ? {
+ mode: "AUTO_ORBIT",
+ autoOrbitAfterSleepMs: 3500,
+ }
+ : {
+ mode: "ORBIT_CONTROLS",
+ autoOrbitAfterSleepMs: 0,
+ },
+ );
+ }}
+ />
+
+
+
+
+
+
+
);
};
diff --git a/app/src/components/controls/visualsDock.tsx b/app/src/components/controls/visualsDock.tsx
deleted file mode 100644
index 71bc3234..00000000
--- a/app/src/components/controls/visualsDock.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import { type HTMLAttributes } from "react";
-import {
- AVAILABLE_VISUALS,
- VISUAL,
- type VisualType,
-} from "@/components/visualizers/common";
-import { useVisualContext, useVisualContextSetters } from "@/context/visual";
-import {
- Box,
- Boxes,
- CircleDashed,
- Dna,
- Footprints,
- Globe,
- Grid3x3,
- HelpCircle,
- Ribbon,
-} from "lucide-react";
-
-import { Dock, DockItem, DockNav } from "./dock";
-
-const VisualIcon = ({ visual }: { visual: VisualType }) => {
- switch (visual) {
- case VISUAL.GRID:
- return
;
- case VISUAL.CUBE:
- return
;
- case VISUAL.SPHERE:
- return
;
- case VISUAL.DIFFUSED_RING:
- return
;
- case VISUAL.DNA:
- return
;
- case VISUAL.BOXES:
- return
;
- case VISUAL.RIBBONS:
- return
;
- case VISUAL.WALK:
- return
;
- default:
- return
;
- }
-};
-
-export const VisualsDock = ({ ...props }: HTMLAttributes
) => {
- const { visual: activeVisual } = useVisualContext();
- const { setVisual } = useVisualContextSetters();
-
- return (
-
-
- {AVAILABLE_VISUALS.map((visual) => (
- setVisual(visual)}
- >
-
-
- ))}
-
-
- );
-};
-
-export default VisualsDock;
diff --git a/app/src/components/ui/dock.tsx b/app/src/components/ui/dock.tsx
new file mode 100644
index 00000000..41b28819
--- /dev/null
+++ b/app/src/components/ui/dock.tsx
@@ -0,0 +1,392 @@
+"use client";
+
+/* eslint-disable */
+import {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+ type ReactNode,
+} from "react";
+import { cn } from "@/lib/utils";
+import {
+ animate,
+ AnimatePresence,
+ motion,
+ useAnimation,
+ useMotionValue,
+ useSpring,
+ useTransform,
+ type MotionValue,
+} from "framer-motion";
+
+// Interface to define the types for our Dock context
+interface DockContextType {
+ width: number; // Width of the dock
+ hovered: boolean; // If the dock is hovered
+ setIsZooming: (value: boolean) => void; // Function to set zooming state
+ zoomLevel: MotionValue; // Motion value for zoom level
+ mouseX: MotionValue; // Motion value for mouse X position
+}
+
+// Initial width for the dock
+const INITIAL_WIDTH = 48;
+
+// Create a context to manage Dock state
+const DockContext = createContext({
+ width: 0,
+ hovered: false,
+ /* eslint-disable @typescript-eslint/no-empty-function */
+ setIsZooming: () => {},
+ zoomLevel: null as any,
+ mouseX: null as any,
+});
+
+// Custom hook to use Dock context
+const useDock = () => useContext(DockContext);
+
+// Props for the Dock component
+interface DockProps {
+ className?: string;
+ children: ReactNode; // React children to be rendered within the dock
+ fixedChildren: ReactNode;
+}
+
+// Main Dock component: orchestrating the dock's animation behavior
+function Dock({ className, children, fixedChildren }: DockProps) {
+ const [hovered, setHovered] = useState(false); // State to track if the dock is hovered. When the mouse hovers over the dock, this state changes to true.
+ const [width, setWidth] = useState(0); // State to track the width of the dock. This dynamically updates based on the dock's current width.
+ const dockRef = useRef(null); // Reference to the dock element in the DOM. This allows direct manipulation and measurement of the dock.
+ const isZooming = useRef(false); // Reference to track if the zooming animation is active. This prevents conflicting animations.
+
+ // Callback to toggle the zooming state. This ensures that we don't trigger hover animations while zooming.
+ const setIsZooming = useCallback((value: boolean) => {
+ isZooming.current = value; // Update the zooming reference
+ setHovered(!value); // Update the hover state based on zooming
+ }, []);
+
+ const zoomLevel = useMotionValue(1); // Motion value for the zoom level of the dock. This provides a smooth zooming animation.
+
+ // Hook to handle window resize events and update the dock's width accordingly.
+ useWindowResize(() => {
+ setWidth(dockRef.current?.clientWidth || 0); // Set width to the dock's current width or 0 if undefined
+ });
+
+ const mouseX = useMotionValue(Infinity); // Motion value to track the mouse's X position relative to the viewport. Initialized to Infinity to denote no tracking initially.
+
+ return (
+ // Provide the dock's state and control methods to the rest of the application through context.
+
+ {
+ mouseX.set(e.pageX); // Update the mouseX motion value to the current mouse position
+ if (!isZooming.current) {
+ // Only set hovered if not zooming
+ setHovered(true); // Set hovered state to true
+ } else {
+ // setHovered(false); // Set hovered state to false
+ }
+ }}
+ // Event handler for when the mouse leaves the dock
+ onMouseLeave={() => {
+ mouseX.set(Infinity); // Reset mouseX motion value
+ setHovered(false); // Set hovered state to false
+ }}
+ style={{
+ x: "-50%", // Center the dock horizontally
+ scale: zoomLevel, // Bind the zoom level to the scale style property
+ }}
+ >
+
+ {children}
+
+ {fixedChildren && (
+ <>
+ {fixedChildren}
+ >
+ )}
+
+
+ );
+}
+
+// Props for the DockCard component
+interface DockCardProps {
+ className?: string; // Additional classes to be applied to the dock card
+ children: ReactNode; // React children to be rendered within the dock card
+ id: string; // Unique identifier for the dock card
+ active?: boolean;
+ handleClick?: () => void;
+}
+
+// DockCard component: manages individual card behavior within the dock
+function DockCard({
+ className,
+ children,
+ id,
+ handleClick,
+ active = false,
+}: DockCardProps) {
+ const cardRef = useRef(null); // Reference to the card button element for direct DOM manipulation and measurement
+ const [elCenterX, setElCenterX] = useState(0); // State to store the center X position of the card for accurate mouse interaction calculations
+ const dock = useDock(); // Access the Dock context to get shared state and control functions
+
+ // Spring animation for the size of the card, providing a smooth and responsive scaling effect
+ const size = useSpring(INITIAL_WIDTH, {
+ stiffness: 320,
+ damping: 20,
+ mass: 0.1,
+ });
+
+ // Spring animation for the opacity of the card, enabling smooth fade-in and fade-out effects
+ const opacity = useSpring(0, {
+ stiffness: 300,
+ damping: 20,
+ });
+
+ // Custom hook to track mouse position and update the card size dynamically based on proximity to the mouse
+ useMousePosition(
+ {
+ onChange: ({ value }) => {
+ const mouseX = value.x;
+ if (dock.width > 0) {
+ // Calculate transformed value based on mouse position and card center, using a cosine function for a smooth curve
+ const transformedValue =
+ INITIAL_WIDTH +
+ 36 *
+ Math.cos((((mouseX - elCenterX) / dock.width) * Math.PI) / 2) **
+ 12;
+
+ // Only animate size if the dock is hovered
+ if (dock.hovered) {
+ animate(size, transformedValue);
+ }
+ }
+ },
+ },
+ [elCenterX, dock],
+ );
+
+ // Hook to update the center X position of the card on window resize for accurate mouse interaction
+ useWindowResize(() => {
+ const { x } = cardRef.current?.getBoundingClientRect() || { x: 0 };
+ setElCenterX(x + 24); // 24 is the half of INITIAL_WIDTH (48 / 2), centering the element
+ });
+
+ const isAnimating = useRef(false); // Reference to track if the card is currently animating to avoid conflicting animations
+ const controls = useAnimation(); // Animation controls for managing card's Y position during the animation loop
+ const timeoutRef = useRef(null); // Reference to manage timeout cleanup on component unmount
+
+ // Handle click event to start or stop the card's animation
+ useEffect(() => {
+ if (active === isAnimating.current) {
+ return;
+ }
+ if (active) {
+ isAnimating.current = true;
+ opacity.set(0.5); // Set opacity for the animation
+ controls.start({
+ y: -5, // Move the card up by 24 pixels
+ transition: {
+ repeat: Infinity, // Repeat the animation indefinitely
+ repeatType: "reverse", // Reverse the direction of the animation each cycle
+ duration: 0.75, // Duration of each cycle
+ },
+ });
+ } else {
+ isAnimating.current = false;
+ opacity.set(0); // Reset opacity
+ controls.start({
+ y: 0, // Reset Y position to the original state
+ transition: { duration: 0.75 }, // Smooth transition back to original state
+ });
+ }
+ }, [active]);
+
+ // Cleanup timeout on component unmount to prevent memory leaks
+ useEffect(() => {
+ return () => clearTimeout(timeoutRef.current!);
+ }, []);
+
+ // Calculate the distance from the mouse position to the center of the card
+ const distance = useTransform(dock.mouseX, (val) => {
+ const bounds = cardRef.current?.getBoundingClientRect() ?? {
+ x: 0,
+ width: 0,
+ };
+ return val - bounds.x - bounds.width / 2; // Calculate distance to the center of the card
+ });
+
+ // Transform the calculated distance into a responsive width for the card
+ let widthSync = useTransform(distance, [-150, 0, 150], [40, 80, 40]);
+ let width = useSpring(widthSync, { mass: 0.1, stiffness: 150, damping: 12 });
+
+ return (
+
+ {/* Motion button for the card, handling click events and animations */}
+
handleClick() : undefined}
+ style={{
+ width: width, // Responsive width based on mouse distance
+ }}
+ animate={controls} // Animation controls for Y position
+ whileTap={{ scale: 0.95 }} // Scale down slightly on tap for a tactile feel
+ >
+ {children}{" "}
+ {/* Render the children of the DockCard inside the motion button */}
+
+
+ {/* AnimatePresence to manage the presence and layout animations of the card's indicator */}
+
+ {isAnimating.current ? (
+
+
+
+ ) : null}
+
+
+ );
+}
+
+// Divider component for the dock
+function DockDivider() {
+ return (
+
+
+
+ );
+}
+
+type UseWindowResizeCallback = (width: number, height: number) => void;
+
+// Custom hook to handle window resize events and invoke a callback with the new dimensions
+function useWindowResize(callback: UseWindowResizeCallback) {
+ // Create a stable callback reference to ensure the latest callback is always used
+ const callbackRef = useCallbackRef(callback);
+
+ useEffect(() => {
+ // Function to handle window resize and call the provided callback with updated dimensions
+ const handleResize = () => {
+ callbackRef(window.innerWidth, window.innerHeight);
+ };
+
+ // Initial call to handleResize to capture the current window size
+ handleResize();
+ // Adding event listener for window resize events
+ window.addEventListener("resize", handleResize);
+
+ // Cleanup function to remove the event listener when the component unmounts or dependencies change
+ return () => {
+ window.removeEventListener("resize", handleResize);
+ };
+ }, [callbackRef]); // Dependency array includes the stable callback reference
+}
+
+// Custom hook to create a stable callback reference
+function useCallbackRef any>(callback: T): T {
+ // Use a ref to store the callback
+ const callbackRef = useRef(callback);
+
+ // Update the ref with the latest callback whenever it changes
+ useEffect(() => {
+ callbackRef.current = callback;
+ });
+
+ // Return a memoized version of the callback that always uses the latest callback stored in the ref
+ return useMemo(() => ((...args) => callbackRef.current?.(...args)) as T, []);
+}
+
+// Interface for mouse position options
+interface MousePositionOptions {
+ onChange?: (position: { value: { x: number; y: number } }) => void;
+}
+
+// Custom hook to track mouse position and animate values accordingly
+export function useMousePosition(
+ options: MousePositionOptions = {}, // Options to customize behavior, including an onChange callback
+ deps: readonly any[] = [], // Dependencies array to determine when the effect should re-run
+) {
+ const { onChange } = options; // Destructure onChange from options for use in the effect
+
+ // Create motion values for x and y coordinates, initialized to 0
+ const x = useMotionValue(0);
+ const y = useMotionValue(0);
+
+ useEffect(() => {
+ // Function to handle mouse move events, animating the x and y motion values to the current mouse coordinates
+ const handleMouseMove = (event: MouseEvent) => {
+ animate(x, event.clientX);
+ animate(y, event.clientY);
+ };
+
+ // Function to handle changes in the motion values, calling the onChange callback if it exists
+ const handleChange = () => {
+ if (onChange) {
+ onChange({ value: { x: x.get(), y: y.get() } });
+ }
+ };
+
+ // Subscribe to changes in the x and y motion values
+ const unsubscribeX = x.on("change", handleChange);
+ const unsubscribeY = y.on("change", handleChange);
+
+ // Add event listener for mouse move events
+ window.addEventListener("mousemove", handleMouseMove);
+
+ // Cleanup function to remove event listener and unsubscribe from motion value changes
+ return () => {
+ window.removeEventListener("mousemove", handleMouseMove);
+ unsubscribeX();
+ unsubscribeY();
+ };
+ }, [x, y, onChange, ...deps]); // Dependency array includes x, y, onChange, and any additional dependencies
+
+ // Memoize and return the motion values for x and y coordinates
+ return useMemo(
+ () => ({
+ x, // Motion value for x coordinate
+ y, // Motion value for y coordinate
+ }),
+ [x, y], // Dependencies for the memoized return value
+ );
+}
+
+export { Dock, DockCard, DockDivider, useDock };
diff --git a/app/src/components/visualizers/audioScope/base.tsx b/app/src/components/visualizers/audioScope/base.tsx
index f83280dc..c8552ac7 100644
--- a/app/src/components/visualizers/audioScope/base.tsx
+++ b/app/src/components/visualizers/audioScope/base.tsx
@@ -1,75 +1,11 @@
import { Fragment, useEffect, useMemo, useRef } from "react";
+import { type TextureMapper } from "@/lib/mappers/textureMappers/textureMapper";
import { useFrame, useThree } from "@react-three/fiber";
-import {
- Color,
- DataTexture,
- RGBAFormat,
- Vector2,
- Vector3,
- type ShaderMaterial,
-} from "three";
+import { Color, Vector2, Vector3, type ShaderMaterial } from "three";
import fragmentShader from "./shaders/fragment";
import vertexShader from "./shaders/vertex";
-export class TextureMapper {
- public samplesX: Float32Array;
- public samplesY: Float32Array;
- public maxAmplitude = 4.0;
- private readonly M: number = 4;
-
- constructor(samplesX: Float32Array, samplesY: Float32Array) {
- if (samplesX.length != samplesY.length) {
- throw new Error("sample size mismatch");
- }
- this.samplesX = samplesX;
- this.samplesY = samplesY;
- }
-
- public updateTextureData(data: Uint8Array): void {
- const B = (1 << 16) - 1;
- let j, x, y;
- for (let i = 0; i < this.samplesX.length; i++) {
- x = Math.max(
- 0,
- Math.min(
- 2 * this.maxAmplitude,
- 0.5 + (0.5 * this.samplesX[i]) / this.maxAmplitude,
- ),
- );
- y = Math.max(
- 0,
- Math.min(
- 2 * this.maxAmplitude,
- 0.5 + (0.5 * this.samplesY[i]) / this.maxAmplitude,
- ),
- );
-
- x = (x * B) | 0;
- y = (y * B) | 0;
- j = i * this.M;
- data[j + 0] = x >> 8;
- data[j + 1] = x & 0xff;
- data[j + 2] = y >> 8;
- data[j + 3] = y & 0xff;
- }
- }
-
- public generateSupportedTextureAndData() {
- const textureData = new Uint8Array(this.samplesX.length * this.M);
- const tex = new DataTexture(
- textureData,
- this.samplesX.length,
- 1,
- RGBAFormat,
- );
- return {
- tex: tex,
- textureData: textureData,
- };
- }
-}
-
const BaseScopeVisual = ({
textureMapper,
nParticles = 512,
diff --git a/app/src/components/visualizers/audioScope/index.tsx b/app/src/components/visualizers/audioScope/index.tsx
new file mode 100644
index 00000000..ce5fb412
--- /dev/null
+++ b/app/src/components/visualizers/audioScope/index.tsx
@@ -0,0 +1,29 @@
+import { lazy, Suspense, useMemo } from "react";
+import { APPLICATION_MODE } from "@/lib/applicationModes";
+import { Shell } from "lucide-react";
+
+import { type TVisualProps } from "../models";
+
+const ReactiveComponent = (props: TVisualProps) => {
+ const VisualComponent = useMemo(
+ () =>
+ lazy(
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+ async () => await import(`./reactive`),
+ ),
+ [],
+ );
+ return (
+
+
+
+ );
+};
+
+export default {
+ id: "scope",
+ icon: Shell,
+ ReactiveComponent,
+ ControlsComponent: null,
+ supportedApplicationModes: [APPLICATION_MODE.AUDIO_SCOPE],
+} as const;
diff --git a/app/src/components/visualizers/audioScope/reactive.tsx b/app/src/components/visualizers/audioScope/reactive.tsx
index 131ca609..23a589f2 100644
--- a/app/src/components/visualizers/audioScope/reactive.tsx
+++ b/app/src/components/visualizers/audioScope/reactive.tsx
@@ -1,30 +1,18 @@
-import { useEffect } from "react";
-import { useVisualContextSetters } from "@/context/visual";
-import { useAppStateActions, usePalette } from "@/lib/appState";
+import { usePalette } from "@/lib/appState";
import { ColorPalette } from "@/lib/palettes";
-import BaseScopeVisual, { type TextureMapper } from "./base";
+import { type TVisualProps } from "../models";
+import BaseScopeVisual from "./base";
-const ScopeVisual = ({ textureMapper }: { textureMapper: TextureMapper }) => {
+export default ({ textureMapper }: TVisualProps) => {
const palette = usePalette();
- const { setPalette } = useAppStateActions();
- const { setColorBackground } = useVisualContextSetters();
const color = ColorPalette.getPalette(palette).lerpColor(0.5);
- const usePoints = true;
-
- useEffect(() => {
- setPalette("rainbow");
- setColorBackground(false);
- }, [setPalette, setColorBackground]);
-
return (
);
};
-
-export default ScopeVisual;
diff --git a/app/src/components/visualizers/common.ts b/app/src/components/visualizers/common.ts
deleted file mode 100644
index 26c30348..00000000
--- a/app/src/components/visualizers/common.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { type ICoordinateMapper } from "@/lib/mappers/coordinateMappers/common";
-import { type IMotionMapper } from "@/lib/mappers/motionMappers/common";
-import { type IScalarTracker } from "@/lib/mappers/valueTracker/common";
-
-export interface VisualProps {
- coordinateMapper: ICoordinateMapper;
- scalarTracker?: IScalarTracker;
-}
-
-export interface MotionVisualProps {
- motionMapper: IMotionMapper;
- scalarTracker?: IScalarTracker;
-}
-
-export const VISUAL = {
- GRID: "grid",
- SPHERE: "sphere",
- CUBE: "cube",
- DIFFUSED_RING: "diffusedRing",
- DNA: "dna",
- BOXES: "boxes",
- RIBBONS: "ribbons",
- WALK: "walk",
- // STENCIL: "stencil",
- // SWARM: "swarm",
-} as const;
-
-export const AVAILABLE_VISUALS = Object.values(VISUAL);
-export type VisualType = (typeof VISUAL)[keyof typeof VISUAL];
diff --git a/app/src/components/visualizers/cube/controls.tsx b/app/src/components/visualizers/cube/controls.tsx
new file mode 100644
index 00000000..fafe54fb
--- /dev/null
+++ b/app/src/components/visualizers/cube/controls.tsx
@@ -0,0 +1,66 @@
+import { ValueLabel } from "@/components/controls/common";
+import { Button } from "@/components/ui/button";
+import { Label } from "@/components/ui/label";
+import { Slider } from "@/components/ui/slider";
+import { Switch } from "@/components/ui/switch";
+
+import { useActions, useParams, usePresets } from "./reactive";
+
+export default () => {
+ const { nPerSide, cubeSpacingScalar, volume } = useParams();
+ const { active: activePreset, options: presetOptions } = usePresets();
+ const { setParams, setPreset } = useActions();
+
+ return (
+
+
+
+ {[...Object.keys(presetOptions), "custom"].map((p) => (
+
+ ))}
+
+ {!activePreset && (
+ <>
+
+
setParams({ nPerSide: e[0] })}
+ />
+
+ setParams({ cubeSpacingScalar: e[0] })}
+ />
+
+
+ {
+ setParams({ volume: e });
+ }}
+ />
+
+ >
+ )}
+
+ );
+};
diff --git a/app/src/components/visualizers/cube/index.tsx b/app/src/components/visualizers/cube/index.tsx
new file mode 100644
index 00000000..6fea6ad4
--- /dev/null
+++ b/app/src/components/visualizers/cube/index.tsx
@@ -0,0 +1,49 @@
+import { lazy, Suspense, useMemo } from "react";
+import { APPLICATION_MODE } from "@/lib/applicationModes";
+import { Box } from "lucide-react";
+
+import { type TVisualProps } from "../models";
+
+const ReactiveComponent = (props: TVisualProps) => {
+ const VisualComponent = useMemo(
+ () =>
+ lazy(
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+ async () => await import(`./reactive`),
+ ),
+ [],
+ );
+ return (
+
+
+
+ );
+};
+
+const ControlsComponent = () => {
+ const ControlsComponent = useMemo(
+ () =>
+ lazy(
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+ async () => await import(`./controls`),
+ ),
+ [],
+ );
+ return (
+
+
+
+ );
+};
+
+export default {
+ id: "cube",
+ icon: Box,
+ ReactiveComponent,
+ ControlsComponent,
+ supportedApplicationModes: [
+ APPLICATION_MODE.WAVE_FORM,
+ APPLICATION_MODE.NOISE,
+ APPLICATION_MODE.AUDIO,
+ ],
+} as const;
diff --git a/app/src/components/visualizers/cube/reactive.tsx b/app/src/components/visualizers/cube/reactive.tsx
index 6b8c86c2..1b9f23a6 100644
--- a/app/src/components/visualizers/cube/reactive.tsx
+++ b/app/src/components/visualizers/cube/reactive.tsx
@@ -1,34 +1,47 @@
-import { type VisualProps } from "@/components/visualizers/common";
+import { type ComponentPropsWithoutRef } from "react";
import Ground from "@/components/visualizers/ground";
-import { useCubeVisualConfigContext } from "@/context/visualConfig/cube";
+import {
+ type TOmitVisualProps,
+ type TVisualProps,
+} from "@/components/visualizers/models";
+import { createConfigStore } from "@/lib/storeHelpers";
import { Vector3 } from "three";
-import BaseCube from "./base";
+import BaseVisual from "./base";
-const CubeVisual = ({ coordinateMapper }: VisualProps) => {
- const { nPerSide, unitSideLength, unitSpacingScalar, volume } =
- useCubeVisualConfigContext();
+export type TConfig = Required<
+ TOmitVisualProps>
+>;
+
+export const { useParams, useActions, usePresets } = createConfigStore(
+ {
+ default: {
+ nPerSide: 10,
+ cubeSideLength: 0.5,
+ cubeSpacingScalar: 0.1,
+ volume: true,
+ },
+ },
+);
+
+export default ({ coordinateMapper }: TVisualProps) => {
+ const params = useParams();
return (
<>
-
+
>
);
};
-
-export default CubeVisual;
diff --git a/app/src/components/visualizers/diffusedRing/controls.tsx b/app/src/components/visualizers/diffusedRing/controls.tsx
new file mode 100644
index 00000000..0a63ab85
--- /dev/null
+++ b/app/src/components/visualizers/diffusedRing/controls.tsx
@@ -0,0 +1,63 @@
+import { ValueLabel } from "@/components/controls/common";
+import { Button } from "@/components/ui/button";
+import { Label } from "@/components/ui/label";
+import { Slider } from "@/components/ui/slider";
+import { Switch } from "@/components/ui/switch";
+
+import { useActions, useParams, usePresets } from "./reactive";
+
+export default () => {
+ const { radius, pointSize, mirrorEffects } = useParams();
+ const { setParams, setPreset } = useActions();
+ const { active: activePreset, options: presetOptions } = usePresets();
+
+ return (
+
+
+
+ {[...Object.keys(presetOptions), "custom"].map((p) => (
+
+ ))}
+
+ {!activePreset && (
+ <>
+
+
setParams({ radius: e[0] })}
+ />
+
+ setParams({ pointSize: e[0] })}
+ />
+
+
+ {
+ setParams({ mirrorEffects: e });
+ }}
+ />
+
+ >
+ )}
+
+ );
+};
diff --git a/app/src/components/visualizers/diffusedRing/index.tsx b/app/src/components/visualizers/diffusedRing/index.tsx
new file mode 100644
index 00000000..b4a83029
--- /dev/null
+++ b/app/src/components/visualizers/diffusedRing/index.tsx
@@ -0,0 +1,49 @@
+import { lazy, Suspense, useMemo } from "react";
+import { APPLICATION_MODE } from "@/lib/applicationModes";
+import { CircleDashed } from "lucide-react";
+
+import { type TVisualProps } from "../models";
+
+const ReactiveComponent = (props: TVisualProps) => {
+ const VisualComponent = useMemo(
+ () =>
+ lazy(
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+ async () => await import(`./reactive`),
+ ),
+ [],
+ );
+ return (
+
+
+
+ );
+};
+
+const ControlsComponent = () => {
+ const ControlsComponent = useMemo(
+ () =>
+ lazy(
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+ async () => await import(`./controls`),
+ ),
+ [],
+ );
+ return (
+
+
+
+ );
+};
+
+export default {
+ id: "diffusedRing",
+ icon: CircleDashed,
+ ReactiveComponent,
+ ControlsComponent,
+ supportedApplicationModes: [
+ APPLICATION_MODE.WAVE_FORM,
+ APPLICATION_MODE.NOISE,
+ APPLICATION_MODE.AUDIO,
+ ],
+} as const;
diff --git a/app/src/components/visualizers/diffusedRing/reactive.tsx b/app/src/components/visualizers/diffusedRing/reactive.tsx
index 89a20608..ea68fbd0 100644
--- a/app/src/components/visualizers/diffusedRing/reactive.tsx
+++ b/app/src/components/visualizers/diffusedRing/reactive.tsx
@@ -1,28 +1,42 @@
-import { type VisualProps } from "@/components/visualizers/common";
+import { type ComponentPropsWithoutRef } from "react";
import Ground from "@/components/visualizers/ground";
-import { useRingVisualConfigContext } from "@/context/visualConfig/diffusedRing";
+import {
+ type TOmitVisualProps,
+ type TVisualProps,
+} from "@/components/visualizers/models";
+import { createConfigStore } from "@/lib/storeHelpers";
import { Bloom, EffectComposer, Noise } from "@react-three/postprocessing";
import { Vector3 } from "three";
-import BaseDiffusedRing from "./base";
+import BaseVisual from "./base";
-const DiffusedRingVisual = ({ coordinateMapper }: VisualProps) => {
- const { radius, pointSize, mirrorEffects } = useRingVisualConfigContext();
+export type TConfig = Required<
+ TOmitVisualProps>
+>;
+
+export const { useParams, useActions, usePresets } = createConfigStore(
+ {
+ default: {
+ radius: 2,
+ nPoints: 1000,
+ pointSize: 0.2,
+ mirrorEffects: false,
+ },
+ },
+);
+
+const DiffusedRingVisual = ({ coordinateMapper }: TVisualProps) => {
+ const params = useParams();
return (
<>
-
+
>
);
};
-const ComposeDiffusedRingVisual = ({ ...props }: VisualProps) => {
+export default (props: TVisualProps) => {
return (
<>
@@ -33,5 +47,3 @@ const ComposeDiffusedRingVisual = ({ ...props }: VisualProps) => {
>
);
};
-
-export default ComposeDiffusedRingVisual;
diff --git a/app/src/components/visualizers/dna/base.tsx b/app/src/components/visualizers/dna/base.tsx
index 190e09f1..4e076973 100644
--- a/app/src/components/visualizers/dna/base.tsx
+++ b/app/src/components/visualizers/dna/base.tsx
@@ -10,6 +10,7 @@ import { useFrame, type GroupProps } from "@react-three/fiber";
import {
BoxGeometry,
Curve,
+ Euler,
MathUtils,
Matrix4,
MeshBasicMaterial,
@@ -72,7 +73,7 @@ export interface BaseDoubleHelixProps {
fixedBaseGap?: boolean;
}
-const BaseDoubleHelix = forwardRef<
+export const BaseDoubleHelix = forwardRef<
Group,
Omit & BaseDoubleHelixProps
>(
@@ -274,4 +275,76 @@ const BaseDoubleHelix = forwardRef<
);
BaseDoubleHelix.displayName = "BaseDoubleHelix";
-export default BaseDoubleHelix;
+export const MultiStrand = (props: BaseDoubleHelixProps) => {
+ const strandRefs = [
+ useRef(null!),
+ useRef(null!),
+ useRef(null!),
+ useRef(null!),
+ useRef(null!),
+ ];
+ const strandCount = strandRefs.length;
+ const bounds = 15;
+
+ const strandPositions = Array.from({ length: strandCount }).map((_, i) => {
+ return new Vector3()
+ .fromArray(
+ Array.from({ length: 3 }).map(
+ (_, j) => 2 * MathUtils.seededRandom(i + j) - 1,
+ ),
+ )
+ .normalize()
+ .multiplyScalar(bounds);
+ });
+
+ useFrame(({ clock }) => {
+ const t = clock.getElapsedTime();
+ const amplitude = 0.0005;
+ const speed = 0.05;
+ let tmpVec;
+ let norm = 0;
+ strandRefs.forEach((strandRef, strandIdx) => {
+ if (!strandRef.current) {
+ return;
+ }
+ tmpVec = strandPositions[strandIdx];
+ norm = Math.sin(
+ speed * (0.5 + 0.5 * MathUtils.seededRandom(strandIdx)) * t +
+ MathUtils.seededRandom(strandIdx) / speed,
+ );
+ strandRef.current.position.set(
+ tmpVec.x * norm,
+ tmpVec.y * norm,
+ tmpVec.z * norm,
+ );
+ strandRef.current.rotation.z +=
+ amplitude *
+ Math.cos((0.5 + 0.5 * MathUtils.seededRandom(strandIdx)) * t);
+ strandRef.current.rotation.y +=
+ amplitude *
+ Math.cos((0.5 + 0.5 * MathUtils.seededRandom(strandIdx)) * t);
+ });
+ });
+
+ return (
+ <>
+ {strandRefs.map((ref, i) => (
+ Math.PI * (2 * MathUtils.seededRandom(i + j) - 1),
+ ),
+ )
+ }
+ {...props}
+ />
+ ))}
+ >
+ );
+};
+
+export default MultiStrand;
diff --git a/app/src/components/visualizers/dna/index.tsx b/app/src/components/visualizers/dna/index.tsx
new file mode 100644
index 00000000..48f02996
--- /dev/null
+++ b/app/src/components/visualizers/dna/index.tsx
@@ -0,0 +1,33 @@
+import { lazy, Suspense, useMemo } from "react";
+import { APPLICATION_MODE } from "@/lib/applicationModes";
+import { Dna } from "lucide-react";
+
+import { type TVisualProps } from "../models";
+
+const ReactiveComponent = (props: TVisualProps) => {
+ const VisualComponent = useMemo(
+ () =>
+ lazy(
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+ async () => await import(`./reactive`),
+ ),
+ [],
+ );
+ return (
+
+
+
+ );
+};
+
+export default {
+ id: "dna",
+ icon: Dna,
+ ReactiveComponent,
+ ControlsComponent: null,
+ supportedApplicationModes: [
+ APPLICATION_MODE.WAVE_FORM,
+ APPLICATION_MODE.NOISE,
+ APPLICATION_MODE.AUDIO,
+ ],
+} as const;
diff --git a/app/src/components/visualizers/dna/multi.tsx b/app/src/components/visualizers/dna/multi.tsx
deleted file mode 100644
index ce413d45..00000000
--- a/app/src/components/visualizers/dna/multi.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import { useRef } from "react";
-import { useFrame } from "@react-three/fiber";
-import { Euler, MathUtils, Vector3, type Group } from "three";
-
-import BaseDoubleHelix, { type BaseDoubleHelixProps } from "./base";
-
-const MultiStrand = (props: BaseDoubleHelixProps) => {
- const strandRefs = [
- useRef(null!),
- useRef(null!),
- useRef(null!),
- useRef(null!),
- useRef(null!),
- ];
- const strandCount = strandRefs.length;
- const bounds = 15;
-
- const strandPositions = Array.from({ length: strandCount }).map((_, i) => {
- return new Vector3()
- .fromArray(
- Array.from({ length: 3 }).map(
- (_, j) => 2 * MathUtils.seededRandom(i + j) - 1,
- ),
- )
- .normalize()
- .multiplyScalar(bounds);
- });
-
- useFrame(({ clock }) => {
- const t = clock.getElapsedTime();
- const amplitude = 0.0005;
- const speed = 0.05;
- let tmpVec;
- let norm = 0;
- strandRefs.forEach((strandRef, strandIdx) => {
- if (!strandRef.current) {
- return;
- }
- tmpVec = strandPositions[strandIdx];
- norm = Math.sin(
- speed * (0.5 + 0.5 * MathUtils.seededRandom(strandIdx)) * t +
- MathUtils.seededRandom(strandIdx) / speed,
- );
- strandRef.current.position.set(
- tmpVec.x * norm,
- tmpVec.y * norm,
- tmpVec.z * norm,
- );
- strandRef.current.rotation.z +=
- amplitude *
- Math.cos((0.5 + 0.5 * MathUtils.seededRandom(strandIdx)) * t);
- strandRef.current.rotation.y +=
- amplitude *
- Math.cos((0.5 + 0.5 * MathUtils.seededRandom(strandIdx)) * t);
- });
- });
-
- return (
- <>
- {strandRefs.map((ref, i) => (
- Math.PI * (2 * MathUtils.seededRandom(i + j) - 1),
- ),
- )
- }
- {...props}
- />
- ))}
- >
- );
-};
-
-export default MultiStrand;
diff --git a/app/src/components/visualizers/dna/reactive.tsx b/app/src/components/visualizers/dna/reactive.tsx
index 189f19a1..0d32da9e 100644
--- a/app/src/components/visualizers/dna/reactive.tsx
+++ b/app/src/components/visualizers/dna/reactive.tsx
@@ -1,5 +1,8 @@
-import { type VisualProps } from "@/components/visualizers/common";
-import { useDnaVisualConfigContext } from "@/context/visualConfig/dna";
+import {
+ type TOmitVisualProps,
+ type TVisualProps,
+} from "@/components/visualizers/models";
+import { createConfigStore } from "@/lib/storeHelpers";
import {
Bloom,
DepthOfField,
@@ -8,21 +11,32 @@ import {
Vignette,
} from "@react-three/postprocessing";
-import BaseDoubleHelix from "./base";
-import MultiStrand from "./multi";
+import {
+ BaseDoubleHelix,
+ MultiStrand,
+ type BaseDoubleHelixProps,
+} from "./base";
+
+export type TConfig = Required> & {
+ multi: boolean;
+};
+
+export const { useParams, useActions } = createConfigStore({
+ default: {
+ multi: true,
+ helixLength: 50,
+ helixRadius: 1,
+ helixWindingSeparation: 10,
+ strandRadius: 0.1,
+ baseSpacing: 0.35,
+ strandOffsetRad: Math.PI / 2,
+ mirrorEffects: true,
+ fixedBaseGap: false,
+ },
+});
-const DNAVisual = ({ coordinateMapper }: VisualProps) => {
- const {
- multi,
- helixLength,
- helixRadius,
- helixWindingSeparation,
- strandRadius,
- baseSpacing,
- strandOffsetRad,
- mirrorEffects,
- fixedBaseGap,
- } = useDnaVisualConfigContext();
+const DNAVisual = ({ coordinateMapper }: TVisualProps) => {
+ const { multi, ...params } = useParams();
// const {
// multi,
// helixLength,
@@ -56,33 +70,13 @@ const DNAVisual = ({ coordinateMapper }: VisualProps) => {
// });
return multi ? (
-
+
) : (
-
+
);
};
-const ComposedDNAVisual = ({ ...props }: VisualProps) => {
+export default (props: TVisualProps) => {
return (
<>
@@ -100,5 +94,3 @@ const ComposedDNAVisual = ({ ...props }: VisualProps) => {
>
);
};
-
-export default ComposedDNAVisual;
diff --git a/app/src/components/visualizers/grid/controls.tsx b/app/src/components/visualizers/grid/controls.tsx
new file mode 100644
index 00000000..d6068092
--- /dev/null
+++ b/app/src/components/visualizers/grid/controls.tsx
@@ -0,0 +1,65 @@
+import { ValueLabel } from "@/components/controls/common";
+import { Button } from "@/components/ui/button";
+import { Label } from "@/components/ui/label";
+import { Slider } from "@/components/ui/slider";
+
+import { useActions, useParams, usePresets } from "./reactive";
+
+export default () => {
+ const { nGridCols, nGridRows, cubeSpacingScalar } = useParams();
+ const { setParams, setPreset } = useActions();
+ const { active: activePreset, options: presetOptions } = usePresets();
+
+ return (
+
+
+
+ {[...Object.keys(presetOptions), "custom"].map((p) => (
+
+ ))}
+
+ {!activePreset && (
+ <>
+
+
setParams({ nGridRows: e[0] })}
+ />
+
+ setParams({ nGridCols: e[0] })}
+ />
+
+ setParams({ cubeSpacingScalar: e[0] })}
+ />
+ >
+ )}
+
+ );
+};
diff --git a/app/src/components/visualizers/grid/index.tsx b/app/src/components/visualizers/grid/index.tsx
new file mode 100644
index 00000000..32305120
--- /dev/null
+++ b/app/src/components/visualizers/grid/index.tsx
@@ -0,0 +1,49 @@
+import { lazy, Suspense, useMemo } from "react";
+import { APPLICATION_MODE } from "@/lib/applicationModes";
+import { Grid3x3 } from "lucide-react";
+
+import { type TVisualProps } from "../models";
+
+const ReactiveComponent = (props: TVisualProps) => {
+ const VisualComponent = useMemo(
+ () =>
+ lazy(
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+ async () => await import(`./reactive`),
+ ),
+ [],
+ );
+ return (
+
+
+
+ );
+};
+
+const ControlsComponent = () => {
+ const ControlsComponent = useMemo(
+ () =>
+ lazy(
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+ async () => await import(`./controls`),
+ ),
+ [],
+ );
+ return (
+
+
+
+ );
+};
+
+export default {
+ id: "grid",
+ icon: Grid3x3,
+ ReactiveComponent,
+ ControlsComponent,
+ supportedApplicationModes: [
+ APPLICATION_MODE.WAVE_FORM,
+ APPLICATION_MODE.NOISE,
+ APPLICATION_MODE.AUDIO,
+ ],
+} as const;
diff --git a/app/src/components/visualizers/grid/reactive.tsx b/app/src/components/visualizers/grid/reactive.tsx
index 1a7a6ecf..5c582d2e 100644
--- a/app/src/components/visualizers/grid/reactive.tsx
+++ b/app/src/components/visualizers/grid/reactive.tsx
@@ -1,22 +1,40 @@
-import { type VisualProps } from "@/components/visualizers/common";
+import { type ComponentPropsWithoutRef } from "react";
import Ground from "@/components/visualizers/ground";
-import { useGridVisualConfigContext } from "@/context/visualConfig/grid";
+import {
+ type TOmitVisualProps,
+ type TVisualProps,
+} from "@/components/visualizers/models";
+import { createConfigStore } from "@/lib/storeHelpers";
import { Vector3 } from "three";
-import BaseGrid from "./base";
+import BaseVisual from "./base";
-const GridVisual = ({ coordinateMapper }: VisualProps) => {
- const { nRows, nCols, unitSideLength, unitSpacingScalar } =
- useGridVisualConfigContext();
+export type TConfig = Required<
+ TOmitVisualProps>
+>;
+
+export const { useParams, useActions, usePresets } = createConfigStore(
+ {
+ default: {
+ nGridCols: 100,
+ nGridRows: 100,
+ cubeSideLength: 0.025,
+ cubeSpacingScalar: 5,
+ },
+ bands: {
+ nGridRows: 5,
+ nGridCols: 200,
+ cubeSideLength: 0.025,
+ cubeSpacingScalar: 1,
+ },
+ },
+);
+
+const GridVisual = ({ coordinateMapper }: TVisualProps) => {
+ const params = useParams();
return (
<>
-
+
>
);
diff --git a/app/src/components/visualizers/models.ts b/app/src/components/visualizers/models.ts
new file mode 100644
index 00000000..d9f351d4
--- /dev/null
+++ b/app/src/components/visualizers/models.ts
@@ -0,0 +1,13 @@
+import { type ICoordinateMapper } from "@/lib/mappers/coordinateMappers/common";
+import { type IMotionMapper } from "@/lib/mappers/motionMappers/common";
+import { type TextureMapper } from "@/lib/mappers/textureMappers/textureMapper";
+import { type IScalarTracker } from "@/lib/mappers/valueTracker/common";
+
+export type TVisualProps = {
+ coordinateMapper: ICoordinateMapper;
+ scalarTracker?: IScalarTracker;
+ textureMapper: TextureMapper;
+ motionMapper: IMotionMapper;
+};
+
+export type TOmitVisualProps = Omit;
diff --git a/app/src/components/visualizers/boxes/base.tsx b/app/src/components/visualizers/movingBoxes/base.tsx
similarity index 98%
rename from app/src/components/visualizers/boxes/base.tsx
rename to app/src/components/visualizers/movingBoxes/base.tsx
index cef45e60..c743b3d3 100644
--- a/app/src/components/visualizers/boxes/base.tsx
+++ b/app/src/components/visualizers/movingBoxes/base.tsx
@@ -62,7 +62,7 @@ const BaseBoxes = ({
}, [lut, nBoxes]);
useFrame(() => {
- if (detector.step(scalarTracker?.getNormalizedValue() ?? 0)) {
+ if (detector.step(scalarTracker?.get() ?? 0)) {
// random jitter in one direction or the other
const [rowJitter, colJitter] =
Math.random() > 0.5 ? [true, false] : [false, true];
diff --git a/app/src/components/visualizers/movingBoxes/index.tsx b/app/src/components/visualizers/movingBoxes/index.tsx
new file mode 100644
index 00000000..450c62e8
--- /dev/null
+++ b/app/src/components/visualizers/movingBoxes/index.tsx
@@ -0,0 +1,33 @@
+import { lazy, Suspense, useMemo } from "react";
+import { APPLICATION_MODE } from "@/lib/applicationModes";
+import { Boxes } from "lucide-react";
+
+import { type TVisualProps } from "../models";
+
+const ReactiveComponent = (props: TVisualProps) => {
+ const VisualComponent = useMemo(
+ () =>
+ lazy(
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+ async () => await import(`./reactive`),
+ ),
+ [],
+ );
+ return (
+
+
+
+ );
+};
+
+export default {
+ id: "movingBoxes",
+ icon: Boxes,
+ ReactiveComponent,
+ ControlsComponent: null,
+ supportedApplicationModes: [
+ APPLICATION_MODE.WAVE_FORM,
+ APPLICATION_MODE.NOISE,
+ APPLICATION_MODE.AUDIO,
+ ],
+} as const;
diff --git a/app/src/components/visualizers/boxes/reactive.tsx b/app/src/components/visualizers/movingBoxes/reactive.tsx
similarity index 55%
rename from app/src/components/visualizers/boxes/reactive.tsx
rename to app/src/components/visualizers/movingBoxes/reactive.tsx
index 8166d4cc..eedd6d8c 100644
--- a/app/src/components/visualizers/boxes/reactive.tsx
+++ b/app/src/components/visualizers/movingBoxes/reactive.tsx
@@ -1,20 +1,22 @@
-import { type VisualProps } from "@/components/visualizers/common";
import Ground from "@/components/visualizers/ground";
+import { type TVisualProps } from "@/components/visualizers/models";
import { Vector3 } from "three";
-import BaseBoxes from "./base";
+import BaseVisual from "./base";
-const BoxesVisual = ({ scalarTracker }: VisualProps) => {
+export default ({ scalarTracker }: TVisualProps) => {
const nBoxes = 200;
const gridSize = 100;
const cellSize = 1;
return (
<>
- Math.sin(0.0025 * Date.now()) + 1,
+ get: () => Math.sin(0.0025 * Date.now()) + 1,
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
+ set: () => {},
}
}
nBoxes={nBoxes}
@@ -25,5 +27,3 @@ const BoxesVisual = ({ scalarTracker }: VisualProps) => {
>
);
};
-
-export default BoxesVisual;
diff --git a/app/src/components/visualizers/registry.tsx b/app/src/components/visualizers/registry.tsx
new file mode 100644
index 00000000..749afd48
--- /dev/null
+++ b/app/src/components/visualizers/registry.tsx
@@ -0,0 +1,26 @@
+import Scope from "./audioScope";
+import Cube from "./cube";
+import DiffusedRing from "./diffusedRing";
+import Dna from "./dna";
+import Grid from "./grid";
+import MovingBoxes from "./movingBoxes";
+import Ribbons from "./ribbons";
+import Sphere from "./sphere";
+import Swarm from "./swarm";
+import Treadmill from "./treadmill";
+
+export const VISUAL_REGISTRY = {
+ [Scope.id]: Scope,
+ [Grid.id]: Grid,
+ [Cube.id]: Cube,
+ [Sphere.id]: Sphere,
+ [DiffusedRing.id]: DiffusedRing,
+ [Dna.id]: Dna,
+ [MovingBoxes.id]: MovingBoxes,
+ [Ribbons.id]: Ribbons,
+ [Treadmill.id]: Treadmill,
+ [Swarm.id]: Swarm,
+} as const;
+
+export type TVisual = (typeof VISUAL_REGISTRY)[keyof typeof VISUAL_REGISTRY];
+export type TVisualId = TVisual["id"];
diff --git a/app/src/components/visualizers/ribbons/index.tsx b/app/src/components/visualizers/ribbons/index.tsx
new file mode 100644
index 00000000..9e5517d1
--- /dev/null
+++ b/app/src/components/visualizers/ribbons/index.tsx
@@ -0,0 +1,33 @@
+import { lazy, Suspense, useMemo } from "react";
+import { APPLICATION_MODE } from "@/lib/applicationModes";
+import { Ribbon } from "lucide-react";
+
+import { type TVisualProps } from "../models";
+
+const ReactiveComponent = (props: TVisualProps) => {
+ const VisualComponent = useMemo(
+ () =>
+ lazy(
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+ async () => await import(`./reactive`),
+ ),
+ [],
+ );
+ return (
+
+
+
+ );
+};
+
+export default {
+ id: "ribbons",
+ icon: Ribbon,
+ ReactiveComponent,
+ ControlsComponent: null,
+ supportedApplicationModes: [
+ APPLICATION_MODE.WAVE_FORM,
+ APPLICATION_MODE.NOISE,
+ APPLICATION_MODE.AUDIO,
+ ],
+} as const;
diff --git a/app/src/components/visualizers/ribbons/reactive.tsx b/app/src/components/visualizers/ribbons/reactive.tsx
index acd566e4..ffef027d 100644
--- a/app/src/components/visualizers/ribbons/reactive.tsx
+++ b/app/src/components/visualizers/ribbons/reactive.tsx
@@ -1,13 +1,11 @@
-import { type VisualProps } from "@/components/visualizers/common";
+import { type TVisualProps } from "@/components/visualizers/models";
import BaseRibbons from "./base";
-const RibbonsVisual = ({ coordinateMapper }: VisualProps) => {
+export default ({ coordinateMapper }: TVisualProps) => {
return (
<>
>
);
};
-
-export default RibbonsVisual;
diff --git a/app/src/components/visualizers/sphere/controls.tsx b/app/src/components/visualizers/sphere/controls.tsx
new file mode 100644
index 00000000..c767e1e8
--- /dev/null
+++ b/app/src/components/visualizers/sphere/controls.tsx
@@ -0,0 +1,54 @@
+import { ValueLabel } from "@/components/controls/common";
+import { Button } from "@/components/ui/button";
+import { Label } from "@/components/ui/label";
+import { Slider } from "@/components/ui/slider";
+
+import { useActions, useParams, usePresets } from "./reactive";
+
+export default () => {
+ const { radius, nPoints } = useParams();
+ const { setParams, setPreset } = useActions();
+ const { active: activePreset, options: presetOptions } = usePresets();
+
+ return (
+
+
+
+ {[...Object.keys(presetOptions), "custom"].map((p) => (
+
+ ))}
+
+ {!activePreset && (
+ <>
+
+
setParams({ nPoints: e[0] })}
+ />
+
+
+ setParams({ radius: e[0] })}
+ />
+ >
+ )}
+
+ );
+};
diff --git a/app/src/components/visualizers/sphere/index.tsx b/app/src/components/visualizers/sphere/index.tsx
new file mode 100644
index 00000000..2e763b36
--- /dev/null
+++ b/app/src/components/visualizers/sphere/index.tsx
@@ -0,0 +1,49 @@
+import { lazy, Suspense, useMemo } from "react";
+import { APPLICATION_MODE } from "@/lib/applicationModes";
+import { Globe } from "lucide-react";
+
+import { type TVisualProps } from "../models";
+
+const ReactiveComponent = (props: TVisualProps) => {
+ const VisualComponent = useMemo(
+ () =>
+ lazy(
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+ async () => await import(`./reactive`),
+ ),
+ [],
+ );
+ return (
+
+
+
+ );
+};
+
+const ControlsComponent = () => {
+ const ControlsComponent = useMemo(
+ () =>
+ lazy(
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+ async () => await import(`./controls`),
+ ),
+ [],
+ );
+ return (
+
+
+
+ );
+};
+
+export default {
+ id: "sphere",
+ icon: Globe,
+ ReactiveComponent,
+ ControlsComponent,
+ supportedApplicationModes: [
+ APPLICATION_MODE.WAVE_FORM,
+ APPLICATION_MODE.NOISE,
+ APPLICATION_MODE.AUDIO,
+ ],
+} as const;
diff --git a/app/src/components/visualizers/sphere/reactive.tsx b/app/src/components/visualizers/sphere/reactive.tsx
index 73fe014d..9fbc7d5f 100644
--- a/app/src/components/visualizers/sphere/reactive.tsx
+++ b/app/src/components/visualizers/sphere/reactive.tsx
@@ -1,28 +1,42 @@
-import { type VisualProps } from "@/components/visualizers/common";
+import { type ComponentPropsWithoutRef } from "react";
import Ground from "@/components/visualizers/ground";
-import { useSphereVisualConfigContext } from "@/context/visualConfig/sphere";
+import {
+ type TOmitVisualProps,
+ type TVisualProps,
+} from "@/components/visualizers/models";
+import { createConfigStore } from "@/lib/storeHelpers";
import { Vector3 } from "three";
-import BaseSphere from "./base";
+import BaseVisual from "./base";
-const SphereVisual = ({ coordinateMapper }: VisualProps) => {
- const { radius, nPoints, unitSideLength } = useSphereVisualConfigContext();
+export type TConfig = Required<
+ TOmitVisualProps>
+>;
+export const { useParams, useActions, usePresets } = createConfigStore(
+ {
+ default: {
+ radius: 2,
+ nPoints: 800,
+ cubeSideLength: 0.05,
+ },
+ },
+);
+
+export default ({ coordinateMapper }: TVisualProps) => {
+ const params = useParams();
return (
<>
-
+
>
);
};
-
-export default SphereVisual;
diff --git a/app/src/context/visualConfig/stencil.tsx b/app/src/components/visualizers/stencil/config.tsx
similarity index 100%
rename from app/src/context/visualConfig/stencil.tsx
rename to app/src/components/visualizers/stencil/config.tsx
diff --git a/app/src/components/visualizers/stencil/reactive.tsx b/app/src/components/visualizers/stencil/reactive.tsx
index 6ae0f2aa..c5a928d8 100644
--- a/app/src/components/visualizers/stencil/reactive.tsx
+++ b/app/src/components/visualizers/stencil/reactive.tsx
@@ -1,13 +1,13 @@
-import { type VisualProps } from "@/components/visualizers/common";
import Ground from "@/components/visualizers/ground";
-import { useStencilVisualConfigContext } from "@/context/visualConfig/stencil";
+import { type TVisualProps } from "@/components/visualizers/models";
import { Vector3 } from "three";
import BaseStencil from "./base";
+import { useStencilVisualConfigContext } from "./config";
import { getPoly2D as getPoly2D_DIAG } from "./polys/diagonal";
import { getPoly2D as getPoly2D_OWL } from "./polys/owl";
-const StencilVisual = ({ coordinateMapper }: VisualProps) => {
+const StencilVisual = ({ coordinateMapper }: TVisualProps) => {
const { pointSize, power, bounds, transitionSpeed } =
useStencilVisualConfigContext();
// const { pointSize, power, bounds, transitionSpeed } = useControls({
diff --git a/app/src/components/visualizers/swarm/base.tsx b/app/src/components/visualizers/swarm/base.tsx
index 1b9b88a9..291d33c9 100644
--- a/app/src/components/visualizers/swarm/base.tsx
+++ b/app/src/components/visualizers/swarm/base.tsx
@@ -3,7 +3,7 @@ import { type IMotionMapper } from "@/lib/mappers/motionMappers/common";
import { useFrame } from "@react-three/fiber";
import { Vector3, type Points } from "three";
-const BaseGrid = ({
+export default ({
motionMapper,
maxPoints = 1000,
pointSize = 0.2,
@@ -70,5 +70,3 @@ const BaseGrid = ({
);
};
-
-export default BaseGrid;
diff --git a/app/src/components/visualizers/swarm/index.tsx b/app/src/components/visualizers/swarm/index.tsx
new file mode 100644
index 00000000..b85d9694
--- /dev/null
+++ b/app/src/components/visualizers/swarm/index.tsx
@@ -0,0 +1,29 @@
+import { lazy, Suspense, useMemo } from "react";
+import { APPLICATION_MODE } from "@/lib/applicationModes";
+import { Drum } from "lucide-react";
+
+import { type TVisualProps } from "../models";
+
+const ReactiveComponent = (props: TVisualProps) => {
+ const VisualComponent = useMemo(
+ () =>
+ lazy(
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+ async () => await import(`./reactive`),
+ ),
+ [],
+ );
+ return (
+
+
+
+ );
+};
+
+export default {
+ id: "swarm",
+ icon: Drum,
+ ReactiveComponent,
+ ControlsComponent: null,
+ supportedApplicationModes: [APPLICATION_MODE.PARTICLE_NOISE],
+} as const;
diff --git a/app/src/components/visualizers/swarm/reactive.tsx b/app/src/components/visualizers/swarm/reactive.tsx
index cd14de0f..d53515e6 100644
--- a/app/src/components/visualizers/swarm/reactive.tsx
+++ b/app/src/components/visualizers/swarm/reactive.tsx
@@ -1,12 +1,28 @@
-import { type MotionVisualProps } from "@/components/visualizers/common";
+import { type ComponentPropsWithoutRef } from "react";
import Ground from "@/components/visualizers/ground";
-import { useSwarmVisualConfigContext } from "@/context/visualConfig/swarm";
+import { createConfigStore } from "@/lib/storeHelpers";
import { Vector3 } from "three";
-import BaseSwarm from "./base";
+import { type TOmitVisualProps, type TVisualProps } from "../models";
+import BaseVisual from "./base";
-const SwarmVisual = ({ motionMapper }: MotionVisualProps) => {
- const { maxDim, pointSize } = useSwarmVisualConfigContext();
+export type TConfig = Required<
+ TOmitVisualProps>
+>;
+
+export const { useParams, useActions, usePresets } = createConfigStore(
+ {
+ default: {
+ maxPoints: 1000,
+ pointSize: 0.2,
+ maxDim: 2,
+ color: "white",
+ },
+ },
+);
+
+const SwarmVisual = ({ motionMapper }: TVisualProps) => {
+ const params = useParams();
// const { maxDim, pointSize } = useControls({
// Particles: folder(
// {
@@ -19,12 +35,8 @@ const SwarmVisual = ({ motionMapper }: MotionVisualProps) => {
return (
<>
-
-
+
+
>
);
};
diff --git a/app/src/components/visualizers/walk/horse.png b/app/src/components/visualizers/treadmill/horse.png
similarity index 100%
rename from app/src/components/visualizers/walk/horse.png
rename to app/src/components/visualizers/treadmill/horse.png
diff --git a/app/src/components/visualizers/walk/horse.tsx b/app/src/components/visualizers/treadmill/horse.tsx
similarity index 95%
rename from app/src/components/visualizers/walk/horse.tsx
rename to app/src/components/visualizers/treadmill/horse.tsx
index 29a1006c..83080812 100644
--- a/app/src/components/visualizers/walk/horse.tsx
+++ b/app/src/components/visualizers/treadmill/horse.tsx
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef } from "react";
-import { type VisualProps } from "@/components/visualizers/common";
+import { type TVisualProps } from "@/components/visualizers/models";
import { usePalette } from "@/lib/appState";
import { ColorPalette } from "@/lib/palettes";
import { useAnimations, useGLTF } from "@react-three/drei";
@@ -23,7 +23,7 @@ interface GLTFAction extends THREE.AnimationClip {
name: ActionName;
}
-const Horse = (_: VisualProps) => {
+const Horse = (_: TVisualProps) => {
const group = useRef(null);
const { nodes, animations } = useGLTF(MODEL_HORSE) as GLTFResult;
const palette = usePalette();
diff --git a/app/src/components/visualizers/treadmill/index.tsx b/app/src/components/visualizers/treadmill/index.tsx
new file mode 100644
index 00000000..cdcc2ee5
--- /dev/null
+++ b/app/src/components/visualizers/treadmill/index.tsx
@@ -0,0 +1,33 @@
+import { lazy, Suspense, useMemo } from "react";
+import { APPLICATION_MODE } from "@/lib/applicationModes";
+import { Footprints } from "lucide-react";
+
+import { type TVisualProps } from "../models";
+
+const ReactiveComponent = (props: TVisualProps) => {
+ const VisualComponent = useMemo(
+ () =>
+ lazy(
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
+ async () => await import(`./reactive`),
+ ),
+ [],
+ );
+ return (
+
+
+
+ );
+};
+
+export default {
+ id: "treadmill",
+ icon: Footprints,
+ ReactiveComponent,
+ ControlsComponent: null,
+ supportedApplicationModes: [
+ APPLICATION_MODE.WAVE_FORM,
+ APPLICATION_MODE.NOISE,
+ APPLICATION_MODE.AUDIO,
+ ],
+} as const;
diff --git a/app/src/components/visualizers/walk/reactive.tsx b/app/src/components/visualizers/treadmill/reactive.tsx
similarity index 52%
rename from app/src/components/visualizers/walk/reactive.tsx
rename to app/src/components/visualizers/treadmill/reactive.tsx
index fcea4ec9..388c4ae7 100644
--- a/app/src/components/visualizers/walk/reactive.tsx
+++ b/app/src/components/visualizers/treadmill/reactive.tsx
@@ -1,9 +1,9 @@
-import { type VisualProps } from "@/components/visualizers/common";
+import { type TVisualProps } from "@/components/visualizers/models";
import Horse from "./horse";
import { Treadmill } from "./treadmill";
-const WalkVisual = ({ ...props }: VisualProps) => {
+export default ({ ...props }: TVisualProps) => {
return (
<>
@@ -11,5 +11,3 @@ const WalkVisual = ({ ...props }: VisualProps) => {
>
);
};
-
-export default WalkVisual;
diff --git a/app/src/components/visualizers/walk/treadmill.tsx b/app/src/components/visualizers/treadmill/treadmill.tsx
similarity index 98%
rename from app/src/components/visualizers/walk/treadmill.tsx
rename to app/src/components/visualizers/treadmill/treadmill.tsx
index 73c59cef..f55deb2c 100644
--- a/app/src/components/visualizers/walk/treadmill.tsx
+++ b/app/src/components/visualizers/treadmill/treadmill.tsx
@@ -14,7 +14,7 @@ import {
type InstancedMesh,
} from "three";
-import { type VisualProps } from "../common";
+import { type TVisualProps } from "../models";
const curvePoints = [
[-1, 1],
@@ -32,7 +32,7 @@ export const Treadmill = ({
stoneHeight = 0.1,
stoneLength = 1,
coordinateMapper,
-}: VisualProps & {
+}: TVisualProps & {
nStones?: number;
stoneWidth?: number;
stoneHeight?: number;
diff --git a/app/src/components/visualizers/visual.tsx b/app/src/components/visualizers/visual.tsx
deleted file mode 100644
index 628974e7..00000000
--- a/app/src/components/visualizers/visual.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import { Suspense } from "react";
-import { useVisualComponent } from "@/hooks/useVisualComponent";
-import { type ICoordinateMapper } from "@/lib/mappers/coordinateMappers/common";
-import { type IScalarTracker } from "@/lib/mappers/valueTracker/common";
-
-import { type VisualType } from "./common";
-
-export const Visual = ({
- visual,
- coordinateMapper,
- scalarTracker,
-}: {
- visual: VisualType;
- coordinateMapper?: ICoordinateMapper;
- scalarTracker?: IScalarTracker;
-}) => {
- const VisualComponent = useVisualComponent(visual);
- return (
-
-
-
- );
-};
diff --git a/app/src/components/visualizers/visualizerAudio.tsx b/app/src/components/visualizers/visualizerAudio.tsx
deleted file mode 100644
index 35950ad9..00000000
--- a/app/src/components/visualizers/visualizerAudio.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { type VisualType } from "@/components/visualizers/common";
-import { useFFTAnalyzerContext } from "@/context/fftAnalyzer";
-import { useEnergyInfo, useVisualSourceDataX } from "@/lib/appState";
-import { CoordinateMapper_Data } from "@/lib/mappers/coordinateMappers/data";
-import { EnergyTracker } from "@/lib/mappers/valueTracker/energyTracker";
-
-import { Visual } from "./visual";
-
-const AudioVisual = ({ visual }: { visual: VisualType }) => {
- const freqData = useVisualSourceDataX();
- const energyInfo = useEnergyInfo();
- // TODO: Find a better place to put amplitude settings for this audio visual
- const { amplitude } = useFFTAnalyzerContext();
-
- const coordinateMapper = new CoordinateMapper_Data(amplitude, freqData);
- const energyTracker = new EnergyTracker(energyInfo);
-
- return (
-
- );
-};
-
-export default AudioVisual;
diff --git a/app/src/components/visualizers/visualizerAudioScope.tsx b/app/src/components/visualizers/visualizerAudioScope.tsx
deleted file mode 100644
index 30a4660b..00000000
--- a/app/src/components/visualizers/visualizerAudioScope.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import { Suspense } from "react";
-import { TextureMapper } from "@/components/visualizers/audioScope/base";
-import ScopeVisual from "@/components/visualizers/audioScope/reactive";
-import { useVisualSourceDataX, useVisualSourceDataY } from "@/lib/appState";
-
-const AudioScopeVisual = () => {
- const timeSamples = useVisualSourceDataX();
- const quadSamples = useVisualSourceDataY();
-
- const textureMapper = new TextureMapper(timeSamples, quadSamples);
-
- return (
-
-
-
- );
-};
-
-export default AudioScopeVisual;
diff --git a/app/src/components/visualizers/visualizerModal.tsx b/app/src/components/visualizers/visualizerModal.tsx
new file mode 100644
index 00000000..eb691622
--- /dev/null
+++ b/app/src/components/visualizers/visualizerModal.tsx
@@ -0,0 +1,49 @@
+import { useMemo } from "react";
+import { APPLICATION_MODE } from "@/lib/applicationModes";
+import { useMappers, useMode, useVisual } from "@/lib/appState";
+import { type ICoordinateMapper } from "@/lib/mappers/coordinateMappers/common";
+import { COORDINATE_MAPPER_REGISTRY } from "@/lib/mappers/coordinateMappers/registry";
+
+const DummyMapper: ICoordinateMapper = { map: () => 0, amplitude: 0 };
+const useVisualParams = () => {
+ const mode = useMode();
+ const rest = useMappers();
+ const coordinateMapperWaveForm =
+ COORDINATE_MAPPER_REGISTRY.waveform.hooks.useInstance();
+ const coordinateMapperNoise =
+ COORDINATE_MAPPER_REGISTRY.noise.hooks.useInstance();
+ const coordinateMapperData =
+ COORDINATE_MAPPER_REGISTRY.data.hooks.useInstance();
+ const coordinateMapper = useMemo(() => {
+ switch (mode) {
+ case APPLICATION_MODE.WAVE_FORM:
+ return coordinateMapperWaveForm;
+ case APPLICATION_MODE.NOISE:
+ return coordinateMapperNoise;
+ case APPLICATION_MODE.AUDIO:
+ return coordinateMapperData;
+ case APPLICATION_MODE.AUDIO_SCOPE:
+ case APPLICATION_MODE.PARTICLE_NOISE:
+ return DummyMapper;
+ default:
+ return mode satisfies never;
+ }
+ }, [
+ mode,
+ coordinateMapperData,
+ coordinateMapperNoise,
+ coordinateMapperWaveForm,
+ ]);
+
+ return {
+ coordinateMapper,
+ ...rest,
+ };
+};
+const ModalVisual = () => {
+ const visual = useVisual();
+ const visualParams = useVisualParams();
+ return ;
+};
+
+export default ModalVisual;
diff --git a/app/src/components/visualizers/visualizerNoise.tsx b/app/src/components/visualizers/visualizerNoise.tsx
deleted file mode 100644
index ace0b68c..00000000
--- a/app/src/components/visualizers/visualizerNoise.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { type VisualType } from "@/components/visualizers/common";
-import { useNoiseGeneratorContext } from "@/context/noiseGenerator";
-import { CoordinateMapper_Noise } from "@/lib/mappers/coordinateMappers/noise";
-
-import { Visual } from "./visual";
-
-const NoiseVisual = ({ visual }: { visual: VisualType }) => {
- const { amplitude, spatialScale, timeScale, nIterations } =
- useNoiseGeneratorContext();
-
- const coordinateMapper = new CoordinateMapper_Noise(
- amplitude,
- spatialScale,
- timeScale,
- nIterations,
- );
-
- return ;
-};
-
-export default NoiseVisual;
diff --git a/app/src/components/visualizers/visualizerParticleNoise.tsx b/app/src/components/visualizers/visualizerParticleNoise.tsx
deleted file mode 100644
index 075a5cbe..00000000
--- a/app/src/components/visualizers/visualizerParticleNoise.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import SwarmVisual from "@/components/visualizers/swarm/reactive";
-import { MotionMapper_Noise } from "@/lib/mappers/motionMappers/curlNoise";
-
-const ParticleNoiseVisual = () => {
- const spatialScale = 2.0;
- const curlAmount = 0.5;
- // const { spatialScale, curlAmount } = useControls({
- // "Particle Noise Generator": folder({
- // spatialScale: {
- // value: 2.0,
- // min: 0.1,
- // max: 5.0,
- // step: 0.1,
- // },
- // curlAmount: {
- // value: 0.5,
- // min: 0.01,
- // max: 1.0,
- // step: 0.01,
- // },
- // }),
- // });
-
- const motionMapper = new MotionMapper_Noise(spatialScale, curlAmount);
- return ;
-};
-
-export default ParticleNoiseVisual;
diff --git a/app/src/components/visualizers/visualizerWaveform.tsx b/app/src/components/visualizers/visualizerWaveform.tsx
deleted file mode 100644
index 4b333d30..00000000
--- a/app/src/components/visualizers/visualizerWaveform.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { type VisualType } from "@/components/visualizers/common";
-import { useWaveGeneratorContext } from "@/context/waveGenerator";
-import { CoordinateMapper_WaveformSuperposition } from "@/lib/mappers/coordinateMappers/waveform";
-
-import { Visual } from "./visual";
-
-const WaveformVisual = ({ visual }: { visual: VisualType }) => {
- const { maxAmplitude, waveformFrequenciesHz, amplitudeSplitRatio } =
- useWaveGeneratorContext();
-
- const coordinateMapper = new CoordinateMapper_WaveformSuperposition(
- waveformFrequenciesHz,
- maxAmplitude,
- amplitudeSplitRatio,
- );
-
- return ;
-};
-
-export default WaveformVisual;
diff --git a/app/src/context/audioSource.tsx b/app/src/context/audioSource.tsx
deleted file mode 100644
index f6cf385b..00000000
--- a/app/src/context/audioSource.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import {
- createContext,
- useContext,
- useState,
- type Dispatch,
- type PropsWithChildren,
- type SetStateAction,
-} from "react";
-import {
- getPlatformSupportedAudioSources,
- type AudioSource,
-} from "@/components/audio/sourceControls/common";
-
-export interface AudioSourceConfig {
- audioSource: AudioSource;
-}
-
-export const AudioSourceContext = createContext<{
- config: AudioSourceConfig;
- setters: {
- setAudioSource: Dispatch>;
- };
-} | null>(null);
-
-export const AudioSourceContextProvider = ({
- initial = undefined,
- children,
-}: PropsWithChildren<{
- initial?: Partial;
-}>) => {
- const [audioSource, setAudioSource] = useState(
- initial?.audioSource ?? getPlatformSupportedAudioSources()[0],
- );
-
- return (
-
- {children}
-
- );
-};
-
-export function useAudioSourceContext() {
- const context = useContext(AudioSourceContext);
- if (!context) {
- throw new Error(
- "useAudioSourceContext must be used within a AudioSourceContextProvider",
- );
- }
- return context.config;
-}
-
-export function useAudioSourceContextSetters() {
- const context = useContext(AudioSourceContext);
- if (!context) {
- throw new Error(
- "useAudioSourceContext must be used within a AudioSourceContextProvider",
- );
- }
- return context.setters;
-}
diff --git a/app/src/context/cameraControls.tsx b/app/src/context/cameraControls.tsx
deleted file mode 100644
index 64523ffd..00000000
--- a/app/src/context/cameraControls.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import {
- createContext,
- useContext,
- useState,
- type Dispatch,
- type PropsWithChildren,
- type SetStateAction,
-} from "react";
-
-export const CAMERA_CONTROLS_MODE = {
- AUTO_ORBIT: "AUTO_ORBIT",
- ORBIT_CONTROLS: "ORBIT_CONTROLS",
-} as const;
-export type CameraControlsMode =
- (typeof CAMERA_CONTROLS_MODE)[keyof typeof CAMERA_CONTROLS_MODE];
-
-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 & {
- initial?: Partial;
-}) => {
- const [mode, setMode] = useState(
- initial?.mode ?? CAMERA_CONTROLS_MODE.ORBIT_CONTROLS,
- );
- const [autoOrbitAfterSleepMs, setAutoOrbitAfterSleepMs] = useState(
- initial?.autoOrbitAfterSleepMs ?? 10000,
- );
-
- return (
-
- {children}
-
- );
-};
-
-export function useCameraControlsContext() {
- const context = useContext(CameraControlsContext);
- if (!context) {
- throw new Error(
- "useCameraControlsContext must be used within a CameraControlsContextProvider",
- );
- }
- return context.config;
-}
-
-export function useCameraControlsContextSetters() {
- const context = useContext(CameraControlsContext);
- if (!context) {
- throw new Error(
- "useCameraControlsContext must be used within a CameraControlsContextProvider",
- );
- }
- return context.setters;
-}
diff --git a/app/src/context/fftAnalyzer.tsx b/app/src/context/fftAnalyzer.tsx
deleted file mode 100644
index 49cc2338..00000000
--- a/app/src/context/fftAnalyzer.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-import {
- createContext,
- useContext,
- useState,
- type Dispatch,
- type PropsWithChildren,
- type SetStateAction,
-} from "react";
-import { type EnergyMeasure, type OctaveBandMode } from "@/lib/analyzers/fft";
-
-interface FFTAnalyzerConfig {
- // TODO: Find a better place to put amplitude for audio visuals
- amplitude: number;
- octaveBandMode: OctaveBandMode;
- energyMeasure: EnergyMeasure;
-}
-
-export const FFTAnalyzerContext = createContext<{
- config: FFTAnalyzerConfig;
- setters: {
- setAmplitude: Dispatch>;
- setOctaveBand: Dispatch>;
- setEnergyMeasure: Dispatch>;
- };
-} | null>(null);
-
-export const FFTAnalyzerContextProvider = ({
- initial = undefined,
- children,
-}: PropsWithChildren<{
- initial?: Partial;
-}>) => {
- const [amplitude, setAmplitude] = useState(initial?.amplitude ?? 1.0);
- const [octaveBandMode, setOctaveBandMode] = useState(
- initial?.octaveBandMode ?? 2,
- );
- const [energyMeasure, setEnergyMeasure] = useState(
- initial?.energyMeasure ?? "bass",
- );
- return (
-
- {children}
-
- );
-};
-
-export function useFFTAnalyzerContext() {
- const context = useContext(FFTAnalyzerContext);
- if (!context) {
- throw new Error(
- "useFFTAnalyzerContext must be used within a FFTAnalyzerContextProvider",
- );
- }
- return context.config;
-}
-
-export function useFFTAnalyzerContextSetters() {
- const context = useContext(FFTAnalyzerContext);
- if (!context) {
- throw new Error(
- "useFFTAnalyzerContext must be used within a FFTAnalyzerContextProvider",
- );
- }
- return context.setters;
-}
diff --git a/app/src/context/mode.tsx b/app/src/context/mode.tsx
deleted file mode 100644
index 63e4f767..00000000
--- a/app/src/context/mode.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import {
- createContext,
- useContext,
- useState,
- type Dispatch,
- type PropsWithChildren,
- type SetStateAction,
-} from "react";
-import { APPLICATION_MODE, type ApplicationMode } from "@/lib/applicationModes";
-
-export interface ModeConfig {
- mode: ApplicationMode;
- showUI: boolean;
-}
-
-export const ModeContext = createContext<{
- config: ModeConfig;
- setters: {
- setMode: Dispatch>;
- setShowUI: Dispatch>;
- };
-} | null>(null);
-
-export const ModeContextProvider = ({ children }: PropsWithChildren) => {
- const [mode, setMode] = useState(APPLICATION_MODE.WAVE_FORM);
- const [showUI, setShowUI] = useState(true);
-
- return (
-
- {children}
-
- );
-};
-
-export function useModeContext() {
- const context = useContext(ModeContext);
- if (!context) {
- throw new Error("useModeContext must be used within a ModeContextProvider");
- }
- return context.config;
-}
-
-export function useModeContextSetters() {
- const context = useContext(ModeContext);
- if (!context) {
- throw new Error("useModeContext must be used within a ModeContextProvider");
- }
- return context.setters;
-}
diff --git a/app/src/context/noiseGenerator.tsx b/app/src/context/noiseGenerator.tsx
deleted file mode 100644
index b4e95acf..00000000
--- a/app/src/context/noiseGenerator.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-import {
- createContext,
- useContext,
- useState,
- type Dispatch,
- type PropsWithChildren,
- type SetStateAction,
-} from "react";
-
-export interface NoiseGeneratorConfig {
- amplitude: number;
- spatialScale: number;
- timeScale: number;
- nIterations: number;
-}
-
-export const NoiseGeneratorContext = createContext<{
- config: NoiseGeneratorConfig;
- setters: {
- setAmplitude: Dispatch>;
- setSpatialScale: Dispatch>;
- setTimeScale: Dispatch>;
- setNIterations: Dispatch>;
- reset: Dispatch;
- };
-} | null>(null);
-
-export const NoiseGeneratorContextProvider = ({
- initial = undefined,
- children,
-}: PropsWithChildren<{
- initial?: Partial;
-}>) => {
- const [amplitude, setAmplitude] = useState(initial?.amplitude ?? 1.0);
- const [spatialScale, setSpatialScale] = useState(
- initial?.spatialScale ?? 2.0,
- );
- const [timeScale, setTimeScale] = useState(initial?.timeScale ?? 0.5);
- const [nIterations, setNIterations] = useState(
- initial?.nIterations ?? 10,
- );
- return (
- {
- setAmplitude(initial?.amplitude ?? 1.0);
- setSpatialScale(initial?.spatialScale ?? 2.0);
- setTimeScale(initial?.timeScale ?? 0.5);
- setNIterations(initial?.nIterations ?? 10);
- },
- },
- }}
- >
- {children}
-
- );
-};
-
-export function useNoiseGeneratorContext() {
- const context = useContext(NoiseGeneratorContext);
- if (!context) {
- throw new Error(
- "useNoiseGeneratorContext must be used within a NoiseGeneratorContextProvider",
- );
- }
- return context.config;
-}
-
-export function useNoiseGeneratorContextSetters() {
- const context = useContext(NoiseGeneratorContext);
- if (!context) {
- throw new Error(
- "useNoiseGeneratorContext must be used within a NoiseGeneratorContextProvider",
- );
- }
- return context.setters;
-}
diff --git a/app/src/context/visual.tsx b/app/src/context/visual.tsx
deleted file mode 100644
index 08ce7ce6..00000000
--- a/app/src/context/visual.tsx
+++ /dev/null
@@ -1,144 +0,0 @@
-import {
- createContext,
- useContext,
- useEffect,
- useState,
- type Dispatch,
- type PropsWithChildren,
- type SetStateAction,
-} from "react";
-import {
- AVAILABLE_VISUALS,
- VISUAL,
- type VisualType,
-} from "@/components/visualizers/common";
-import { APPLICATION_MODE } from "@/lib/applicationModes";
-
-import { useModeContext } from "./mode";
-import { CubeVisualConfigContextProvider } from "./visualConfig/cube";
-import { RingVisualConfigContextProvider } from "./visualConfig/diffusedRing";
-import { DnaVisualConfigContextProvider } from "./visualConfig/dna";
-import { GridVisualConfigContextProvider } from "./visualConfig/grid";
-import { RibbonsVisualConfigContextProvider } from "./visualConfig/ribbons";
-import { SphereVisualConfigContextProvider } from "./visualConfig/sphere";
-// import { StencilVisualConfigContextProvider } from "./visualConfig/stencil";
-// import { SwarmVisualConfigContextProvider } from "./visualConfig/swarm";
-import { useWaveGeneratorContextSetters } from "./waveGenerator";
-
-interface VisualConfig {
- visual: VisualType;
- colorBackground: boolean;
- paletteTrackEnergy: boolean;
-}
-
-export const VisualContext = createContext<{
- config: VisualConfig;
- setters: {
- setVisual: Dispatch>;
- setColorBackground: Dispatch>;
- setPaletteTrackEnergy: Dispatch>;
- };
-} | null>(null);
-
-export const VisualContextProvider = ({
- initial,
- children,
-}: PropsWithChildren<{
- initial?: Partial;
-}>) => {
- const { mode } = useModeContext();
- const [visual, setVisual] = useState(
- initial?.visual ?? AVAILABLE_VISUALS[0],
- );
- const [colorBackground, setColorBackground] = useState(
- initial?.colorBackground ?? true,
- );
- const [paletteTrackEnergy, setPaletteTrackEnergy] = useState(
- initial?.paletteTrackEnergy ?? false,
- );
- const { setWaveformFrequenciesHz, setMaxAmplitude } =
- useWaveGeneratorContextSetters();
-
- // Reset waveform values whenever the visual changes
- useEffect(() => {
- if (mode === APPLICATION_MODE.WAVE_FORM) {
- switch (visual) {
- case VISUAL.DIFFUSED_RING:
- setWaveformFrequenciesHz([2.0, 10.0]);
- setMaxAmplitude(1.0);
- break;
- default:
- setWaveformFrequenciesHz([2.0]);
- setMaxAmplitude(1.0);
- break;
- }
- }
- }, [visual, mode, setWaveformFrequenciesHz, setMaxAmplitude]);
-
- // Reset paletteTrackEnergy whenever the mode changes
- useEffect(() => {
- switch (mode) {
- case APPLICATION_MODE.WAVE_FORM:
- case APPLICATION_MODE.NOISE:
- case APPLICATION_MODE.AUDIO_SCOPE:
- case APPLICATION_MODE.PARTICLE_NOISE:
- setPaletteTrackEnergy(false);
- break;
- case APPLICATION_MODE.AUDIO:
- setPaletteTrackEnergy(true);
- break;
- default:
- return mode satisfies never;
- }
- }, [mode, setPaletteTrackEnergy]);
- return (
-
-
-
-
-
-
-
- {children}
-
-
-
-
-
-
-
- );
-};
-
-export function useVisualContext() {
- const context = useContext(VisualContext);
- if (!context) {
- throw new Error(
- "useVisualContext must be used within a VisualContextProvider",
- );
- }
- return context.config;
-}
-
-export function useVisualContextSetters() {
- const context = useContext(VisualContext);
- if (!context) {
- throw new Error(
- "useVisualContext must be used within a VisualContextProvider",
- );
- }
- return context.setters;
-}
diff --git a/app/src/context/visualConfig/cube.tsx b/app/src/context/visualConfig/cube.tsx
deleted file mode 100644
index 5b96717c..00000000
--- a/app/src/context/visualConfig/cube.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-import {
- createContext,
- useContext,
- useState,
- type Dispatch,
- type PropsWithChildren,
- type SetStateAction,
-} from "react";
-
-export interface CubeVisualConfig {
- nPerSide: number;
- unitSideLength: number;
- unitSpacingScalar: number;
- volume: boolean;
-}
-
-export const CubeVisualConfigContext = createContext<{
- config: CubeVisualConfig;
- setters: {
- setNPerSide: Dispatch>;
- setUnitSideLength: Dispatch>;
- setUnitSpacingScalar: Dispatch>;
- setVolume: Dispatch>;
- reset: Dispatch;
- };
-} | null>(null);
-
-export const CubeVisualConfigContextProvider = ({
- initial = undefined,
- children,
-}: PropsWithChildren<{
- initial?: Partial;
-}>) => {
- const [nPerSide, setNPerSide] = useState(initial?.nPerSide ?? 10);
- const [unitSideLength, setUnitSideLength] = useState(
- initial?.unitSideLength ?? 0.5,
- );
- const [unitSpacingScalar, setUnitSpacingScalar] = useState(
- initial?.unitSpacingScalar ?? 0.1,
- );
- const [volume, setVolume] = useState(initial?.volume ?? true);
-
- return (
- {
- setNPerSide(initial?.nPerSide ?? 10);
- setUnitSideLength(initial?.unitSideLength ?? 0.5);
- setUnitSpacingScalar(initial?.unitSpacingScalar ?? 0.1);
- setVolume(initial?.volume ?? true);
- },
- },
- }}
- >
- {children}
-
- );
-};
-
-export function useCubeVisualConfigContext() {
- const context = useContext(CubeVisualConfigContext);
- if (!context) {
- throw new Error(
- "useCubeVisualConfigContext must be used within a CubeVisualConfigContextProvider",
- );
- }
- return context.config;
-}
-
-export function useCubeVisualConfigContextSetters() {
- const context = useContext(CubeVisualConfigContext);
- if (!context) {
- throw new Error(
- "useCubeVisualConfigContextSetters must be used within a CubeVisualConfigContextProvider",
- );
- }
- return context.setters;
-}
diff --git a/app/src/context/visualConfig/diffusedRing.tsx b/app/src/context/visualConfig/diffusedRing.tsx
deleted file mode 100644
index 9be58c81..00000000
--- a/app/src/context/visualConfig/diffusedRing.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-import {
- createContext,
- useContext,
- useState,
- type Dispatch,
- type PropsWithChildren,
- type SetStateAction,
-} from "react";
-
-export interface RingVisualConfig {
- radius: number;
- pointSize: number;
- mirrorEffects: boolean;
-}
-
-export const RingVisualConfigContext = createContext<{
- config: RingVisualConfig;
- setters: {
- setRadius: Dispatch>;
- setPointSize: Dispatch>;
- setMirrorEffects: Dispatch>;
- };
-} | null>(null);
-
-export const RingVisualConfigContextProvider = ({
- initial = undefined,
- children,
-}: PropsWithChildren<{
- initial?: Partial;
-}>) => {
- const [radius, setRadius] = useState(initial?.radius ?? 2);
- const [pointSize, setPointSize] = useState(initial?.pointSize ?? 0.2);
- const [mirrorEffects, setMirrorEffects] = useState(
- initial?.mirrorEffects ?? false,
- );
-
- return (
-
- {children}
-
- );
-};
-
-export function useRingVisualConfigContext() {
- const context = useContext(RingVisualConfigContext);
- if (!context) {
- throw new Error(
- "useRingVisualConfigContext must be used within a RingVisualConfigContextProvider",
- );
- }
- return context.config;
-}
-
-export function useRingVisualConfigContextSetters() {
- const context = useContext(RingVisualConfigContext);
- if (!context) {
- throw new Error(
- "useRingVisualConfigContextSetters must be used within a RingVisualConfigContextProvider",
- );
- }
- return context.setters;
-}
diff --git a/app/src/context/visualConfig/dna.tsx b/app/src/context/visualConfig/dna.tsx
deleted file mode 100644
index a64709fe..00000000
--- a/app/src/context/visualConfig/dna.tsx
+++ /dev/null
@@ -1,118 +0,0 @@
-import {
- createContext,
- useContext,
- useState,
- type Dispatch,
- type PropsWithChildren,
- type SetStateAction,
-} from "react";
-
-export interface DnaVisualConfig {
- multi: boolean;
- helixLength: number;
- helixRadius: number;
- helixWindingSeparation: number;
- strandRadius: number;
- baseSpacing: number;
- strandOffsetRad: number;
- mirrorEffects: boolean;
- fixedBaseGap: boolean;
-}
-
-export const DnaVisualConfigContext = createContext<{
- config: DnaVisualConfig;
- setters: {
- setMulti: Dispatch>;
- setHelixLength: Dispatch>;
- setHelixRadius: Dispatch>;
- setHelixWindingSeparation: Dispatch>;
- setStrandRadius: Dispatch>;
- setBaseSpacing: Dispatch>;
- setStrandOffsetRad: Dispatch>;
- setMirrorEffects: Dispatch>;
- setFixedBaseGap: Dispatch>;
- };
-} | null>(null);
-
-export const DnaVisualConfigContextProvider = ({
- initial = undefined,
- children,
-}: PropsWithChildren<{
- initial?: Partial;
-}>) => {
- const [multi, setMulti] = useState(initial?.multi ?? true);
- const [helixLength, setHelixLength] = useState(
- initial?.helixLength ?? 50,
- );
- const [helixRadius, setHelixRadius] = useState(
- initial?.helixRadius ?? 1,
- );
- const [helixWindingSeparation, setHelixWindingSeparation] = useState(
- initial?.helixWindingSeparation ?? 10,
- );
- const [strandRadius, setStrandRadius] = useState(
- initial?.strandRadius ?? 0.1,
- );
- const [baseSpacing, setBaseSpacing] = useState(
- initial?.baseSpacing ?? 0.35,
- );
- const [strandOffsetRad, setStrandOffsetRad] = useState(
- initial?.strandOffsetRad ?? Math.PI / 2,
- );
- const [mirrorEffects, setMirrorEffects] = useState(
- initial?.mirrorEffects ?? true,
- );
- const [fixedBaseGap, setFixedBaseGap] = useState(
- initial?.fixedBaseGap ?? false,
- );
- return (
-
- {children}
-
- );
-};
-
-export function useDnaVisualConfigContext() {
- const context = useContext(DnaVisualConfigContext);
- if (!context) {
- throw new Error(
- "useDnaVisualConfigContext must be used within a DnaVisualConfigContextProvider",
- );
- }
- return context.config;
-}
-
-export function useDnaVisualConfigContextSetters() {
- const context = useContext(DnaVisualConfigContext);
- if (!context) {
- throw new Error(
- "useDnaVisualConfigContextSetters must be used within a DnaVisualConfigContextProvider",
- );
- }
- return context.setters;
-}
diff --git a/app/src/context/visualConfig/grid.tsx b/app/src/context/visualConfig/grid.tsx
deleted file mode 100644
index e53b6dce..00000000
--- a/app/src/context/visualConfig/grid.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-import {
- createContext,
- useContext,
- useState,
- type Dispatch,
- type PropsWithChildren,
- type SetStateAction,
-} from "react";
-
-export interface GridVisualConfig {
- nRows: number;
- nCols: number;
- unitSideLength: number;
- unitSpacingScalar: number;
-}
-
-export const GridVisualConfigContext = createContext<{
- config: GridVisualConfig;
- setters: {
- setNRows: Dispatch>;
- setNCols: Dispatch>;
- setUnitSideLength: Dispatch>;
- setUnitSpacingScalar: Dispatch>;
- reset: Dispatch;
- };
-} | null>(null);
-
-export const GridVisualConfigContextProvider = ({
- initial = undefined,
- children,
-}: PropsWithChildren<{
- initial?: Partial;
-}>) => {
- const [nCols, setNCols] = useState(initial?.nCols ?? 100);
- const [nRows, setNRows] = useState(initial?.nRows ?? 100);
- const [unitSideLength, setUnitSideLength] = useState(
- initial?.unitSideLength ?? 0.025,
- );
- const [unitSpacingScalar, setUnitSpacingScalar] = useState(
- initial?.unitSpacingScalar ?? 5,
- );
-
- return (
- {
- setNCols(initial?.nCols ?? 100);
- setNRows(initial?.nRows ?? 100);
- setUnitSideLength(initial?.unitSideLength ?? 0.025);
- setUnitSpacingScalar(initial?.unitSpacingScalar ?? 5);
- },
- },
- }}
- >
- {children}
-
- );
-};
-
-export function useGridVisualConfigContext() {
- const context = useContext(GridVisualConfigContext);
- if (!context) {
- throw new Error(
- "useGridVisualConfigContext must be used within a GridVisualConfigContextProvider",
- );
- }
- return context.config;
-}
-
-export function useGridVisualConfigContextSetters() {
- const context = useContext(GridVisualConfigContext);
- if (!context) {
- throw new Error(
- "useGridVisualConfigContextSetters must be used within a GridVisualConfigContextProvider",
- );
- }
- return context.setters;
-}
diff --git a/app/src/context/visualConfig/ribbons.tsx b/app/src/context/visualConfig/ribbons.tsx
deleted file mode 100644
index 6b5d8dc1..00000000
--- a/app/src/context/visualConfig/ribbons.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import {
- createContext,
- useContext,
- useState,
- type Dispatch,
- type PropsWithChildren,
- type SetStateAction,
-} from "react";
-
-export interface RibbonsVisualConfig {
- nRibbons: number;
-}
-
-export const RibbonsVisualConfigContext = createContext<{
- config: RibbonsVisualConfig;
- setters: {
- setNRibbons: Dispatch>;
- reset: Dispatch;
- };
-} | null>(null);
-
-export const RibbonsVisualConfigContextProvider = ({
- initial = undefined,
- children,
-}: PropsWithChildren<{
- initial?: Partial;
-}>) => {
- const [nRibbons, setNRibbons] = useState(initial?.nRibbons ?? 5);
- return (
- {
- setNRibbons(initial?.nRibbons ?? 5);
- },
- },
- }}
- >
- {children}
-
- );
-};
-
-export function useRibbonsVisualConfigContext() {
- const context = useContext(RibbonsVisualConfigContext);
- if (!context) {
- throw new Error(
- "useRibbonsVisualConfigContext must be used within a RibbonsVisualConfigContextProvider",
- );
- }
- return context.config;
-}
-
-export function useRibbonsVisualConfigContextSetters() {
- const context = useContext(RibbonsVisualConfigContext);
- if (!context) {
- throw new Error(
- "useRibbonsVisualConfigContextSetters must be used within a RibbonsVisualConfigContextProvider",
- );
- }
- return context.setters;
-}
diff --git a/app/src/context/visualConfig/sphere.tsx b/app/src/context/visualConfig/sphere.tsx
deleted file mode 100644
index 171e6b64..00000000
--- a/app/src/context/visualConfig/sphere.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-import {
- createContext,
- useContext,
- useState,
- type Dispatch,
- type PropsWithChildren,
- type SetStateAction,
-} from "react";
-
-export interface SphereVisualConfig {
- radius: number;
- nPoints: number;
- unitSideLength: number;
-}
-
-export const SphereVisualConfigContext = createContext<{
- config: SphereVisualConfig;
- setters: {
- setRadius: Dispatch>;
- setNPoints: Dispatch>;
- setUnitSideLength: Dispatch>;
- };
-} | null>(null);
-
-export const SphereVisualConfigContextProvider = ({
- initial = undefined,
- children,
-}: PropsWithChildren<{
- initial?: Partial;
-}>) => {
- const [radius, setRadius] = useState(initial?.radius ?? 2);
- const [nPoints, setNPoints] = useState(initial?.nPoints ?? 800);
- const [unitSideLength, setUnitSideLength] = useState(
- initial?.unitSideLength ?? 0.05,
- );
-
- return (
-
- {children}
-
- );
-};
-
-export function useSphereVisualConfigContext() {
- const context = useContext(SphereVisualConfigContext);
- if (!context) {
- throw new Error(
- "useSphereVisualConfigContext must be used within a SphereVisualConfigContextProvider",
- );
- }
- return context.config;
-}
-
-export function useSphereVisualConfigContextSetters() {
- const context = useContext(SphereVisualConfigContext);
- if (!context) {
- throw new Error(
- "useSphereVisualConfigContextSetters must be used within a SphereVisualConfigContextProvider",
- );
- }
- return context.setters;
-}
diff --git a/app/src/context/visualConfig/swarm.tsx b/app/src/context/visualConfig/swarm.tsx
deleted file mode 100644
index ad2eccb9..00000000
--- a/app/src/context/visualConfig/swarm.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-import {
- createContext,
- useContext,
- useState,
- type Dispatch,
- type PropsWithChildren,
- type SetStateAction,
-} from "react";
-
-export interface SwarmVisualConfig {
- maxDim: number;
- pointSize: number;
-}
-
-export const SwarmVisualConfigContext = createContext<{
- config: SwarmVisualConfig;
- setters: {
- setMaxDim: Dispatch>;
- setPointSize: Dispatch>;
- };
-} | null>(null);
-
-export const SwarmVisualConfigContextProvider = ({
- initial = undefined,
- children,
-}: PropsWithChildren<{
- initial?: Partial;
-}>) => {
- const [maxDim, setMaxDim] = useState(initial?.maxDim ?? 10);
- const [pointSize, setPointSize] = useState(initial?.pointSize ?? 0.2);
-
- return (
-
- {children}
-
- );
-};
-
-export function useSwarmVisualConfigContext() {
- const context = useContext(SwarmVisualConfigContext);
- if (!context) {
- throw new Error(
- "useSwarmVisualConfigContext must be used within a SwarmVisualConfigContextProvider",
- );
- }
- return context.config;
-}
-
-export function useSwarmVisualConfigContextSetters() {
- const context = useContext(SwarmVisualConfigContext);
- if (!context) {
- throw new Error(
- "useSwarmVisualConfigContextSetters must be used within a SwarmVisualConfigContextProvider",
- );
- }
- return context.setters;
-}
diff --git a/app/src/context/waveGenerator.tsx b/app/src/context/waveGenerator.tsx
deleted file mode 100644
index 21e4f0a8..00000000
--- a/app/src/context/waveGenerator.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import {
- createContext,
- useContext,
- useState,
- type Dispatch,
- type PropsWithChildren,
- type SetStateAction,
-} from "react";
-
-export interface WaveGeneratorConfig {
- maxAmplitude: number;
- waveformFrequenciesHz: [number, ...number[]];
- amplitudeSplitRatio: number;
-}
-
-export const WaveGeneratorContext = createContext<{
- config: WaveGeneratorConfig;
- setters: {
- setMaxAmplitude: Dispatch>;
- setWaveformFrequenciesHz: Dispatch>;
- setAmplitudeSplitRatio: Dispatch>;
- reset: Dispatch;
- };
-} | null>(null);
-
-export const WaveGeneratorContextProvider = ({
- initial = undefined,
- children,
-}: PropsWithChildren<{
- initial?: Partial;
-}>) => {
- const [maxAmplitude, setMaxAmplitude] = useState(
- initial?.maxAmplitude ?? 1.0,
- );
- const [waveformFrequenciesHz, setWaveformFrequenciesHz] = useState<
- [number, ...number[]]
- >(initial?.waveformFrequenciesHz ?? [2.0]);
-
- const [amplitudeSplitRatio, setAmplitudeSplitRatio] = useState(
- initial?.amplitudeSplitRatio ?? 0.75,
- );
-
- return (
- {
- setMaxAmplitude(initial?.maxAmplitude ?? 1.0);
- setWaveformFrequenciesHz(initial?.waveformFrequenciesHz ?? [2.0]);
- setAmplitudeSplitRatio(initial?.amplitudeSplitRatio ?? 0.75);
- },
- },
- }}
- >
- {children}
-
- );
-};
-
-export function useWaveGeneratorContext() {
- const context = useContext(WaveGeneratorContext);
- if (!context) {
- throw new Error(
- "useWaveGeneratorContext must be used within a WaveGeneratorContextProvider",
- );
- }
- return context.config;
-}
-
-export function useWaveGeneratorContextSetters() {
- const context = useContext(WaveGeneratorContext);
- if (!context) {
- throw new Error(
- "useWaveGeneratorContext must be used within a WaveGeneratorContextProvider",
- );
- }
- return context.setters;
-}
diff --git a/app/src/hooks/useVisualComponent.ts b/app/src/hooks/useVisualComponent.ts
deleted file mode 100644
index cf44c16a..00000000
--- a/app/src/hooks/useVisualComponent.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { lazy, useMemo } from "react";
-import { type VisualType } from "@/components/visualizers/common";
-
-export const useVisualComponent = (visual: VisualType) => {
- return useMemo(
- () =>
- lazy(
- async () =>
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
- await import(`@/components/visualizers/${visual}/reactive.tsx`),
- ),
- [visual],
- );
-};
diff --git a/app/src/lib/analyzers/common.ts b/app/src/lib/analyzers/common.ts
index 6b08d229..968eab4e 100644
--- a/app/src/lib/analyzers/common.ts
+++ b/app/src/lib/analyzers/common.ts
@@ -1,4 +1,4 @@
-export interface AnalyzerInputControl {
+export interface TAnalyzerInputControl {
_audioCtx: AudioContext;
connectInput: (source: AudioNode) => void;
disconnectInputs: () => void;
@@ -7,7 +7,7 @@ export interface AnalyzerInputControl {
export function useMediaStreamLink(
audio: HTMLAudioElement,
- analyzer: AnalyzerInputControl,
+ analyzer: TAnalyzerInputControl,
) {
return {
onDisabled: () => {
diff --git a/app/src/lib/analyzers/fft.ts b/app/src/lib/analyzers/fft.ts
index c1e09016..b747992b 100644
--- a/app/src/lib/analyzers/fft.ts
+++ b/app/src/lib/analyzers/fft.ts
@@ -3,7 +3,7 @@
* See https://github.com/hvianna/audioMotion-analyzer
*/
-import { type AnalyzerInputControl } from "./common";
+import { type TAnalyzerInputControl } from "./common";
export interface FreqBinInfo {
binLo: number;
@@ -60,7 +60,7 @@ export type EnergyMeasure = (typeof EnergyMeasureOptions)[number];
const ROOT24 = 2 ** (1 / 24), // 24th root of 2
C0 = 440 * ROOT24 ** -114; // ~16.35 Hz
-export default class FFTAnalyzer implements AnalyzerInputControl {
+export default class FFTAnalyzer implements TAnalyzerInputControl {
private _analyzer: AnalyserNode;
private _input: GainNode;
private _output: GainNode;
diff --git a/app/src/lib/analyzers/scope.ts b/app/src/lib/analyzers/scope.ts
index e872c93c..2683d2c3 100644
--- a/app/src/lib/analyzers/scope.ts
+++ b/app/src/lib/analyzers/scope.ts
@@ -1,4 +1,4 @@
-import { type AnalyzerInputControl } from "./common";
+import { type TAnalyzerInputControl } from "./common";
function createBufferCopy(context: AudioContext, buffer: Float32Array) {
const copyNode = context.createScriptProcessor(buffer.length, 1, 1);
@@ -47,7 +47,7 @@ function createHilbertFilter(
return [delay, hilbert];
}
-export default class ScopeAnalyzer implements AnalyzerInputControl {
+export default class ScopeAnalyzer implements TAnalyzerInputControl {
public readonly _audioCtx: AudioContext;
public readonly timeSamples: Float32Array;
public readonly quadSamples: Float32Array;
diff --git a/app/src/lib/appState.ts b/app/src/lib/appState.ts
index 243ef5d2..5997cc5f 100644
--- a/app/src/lib/appState.ts
+++ b/app/src/lib/appState.ts
@@ -1,47 +1,134 @@
+import {
+ getPlatformSupportedAudioSources,
+ type TAudioSource,
+} from "@/components/audio/sourceControls/common";
+import {
+ VISUAL_REGISTRY,
+ type TVisual,
+ type TVisualId,
+} from "@/components/visualizers/registry";
import { create } from "zustand";
+import { type EnergyMeasure, type OctaveBandMode } from "./analyzers/fft";
+import { APPLICATION_MODE, type TApplicationMode } from "./applicationModes";
import { EventDetector } from "./eventDetector";
+import { CoordinateMapper_Data } from "./mappers/coordinateMappers/data/mapper";
+import { CoordinateMapper_Noise } from "./mappers/coordinateMappers/noise/mapper";
+import { CoordinateMapper_WaveformSuperposition } from "./mappers/coordinateMappers/waveform/mapper";
+import { type IMotionMapper } from "./mappers/motionMappers/common";
+import { MotionMapper_Noise } from "./mappers/motionMappers/curlNoise";
+import { TextureMapper } from "./mappers/textureMappers/textureMapper";
+import {
+ NoOpScalarTracker,
+ type IScalarTracker,
+} from "./mappers/valueTracker/common";
+import { EnergyTracker } from "./mappers/valueTracker/energyTracker";
import {
AVAILABLE_COLOR_PALETTES,
COLOR_PALETTE,
type ColorPaletteType,
} from "./palettes";
+interface IAppearanceState {
+ showUI: boolean;
+ palette: ColorPaletteType;
+ paletteTrackEnergy: boolean;
+ colorBackground: boolean;
+}
+interface ICameraState {
+ mode: "AUTO_ORBIT" | "ORBIT_CONTROLS";
+ autoOrbitAfterSleepMs: number; // disabled if <= 0
+}
+interface IMappersState {
+ textureMapper: TextureMapper;
+ motionMapper: IMotionMapper;
+ // coordinateMapperWaveform: CoordinateMapper_WaveformSuperposition;
+ // coordinateMapperNoise: CoordinateMapper_Noise;
+ // coordinateMapperData: CoordinateMapper_Data;
+ energyTracker: IScalarTracker | null;
+}
+interface IAudioState {
+ source: TAudioSource;
+}
+interface IAnalyzersState {
+ fft: {
+ octaveBandMode: OctaveBandMode;
+ energyMeasure: EnergyMeasure;
+ };
+}
+
interface IAppState {
user: {
canvasInteractionEventTracker: EventDetector;
};
- visual: {
- palette: ColorPaletteType;
- };
- visualSourceData: {
- x: Float32Array;
- y: Float32Array;
- };
- energyInfo: {
- current: number;
- };
+ visual: TVisual;
+ appearance: IAppearanceState;
+ camera: ICameraState;
+ mode: TApplicationMode;
+ mappers: IMappersState;
+ audio: IAudioState;
+ analyzers: IAnalyzersState;
actions: {
+ setMode: (newMode: TApplicationMode) => void;
+ setAudio: (newAudio: Partial) => void;
+ setVisual: (newVisualId: TVisualId) => void;
+ setCamera: (newCamera: Partial) => void;
noteCanvasInteraction: () => void;
- setPalette: (newPalette: ColorPaletteType) => void;
+ setAppearance: (newAppearance: Partial) => void;
nextPalette: () => void;
- resizeVisualSourceData: (newSize: number) => void;
+ setMappers: (newMappers: Partial) => void;
+ setAnalyzerFFT: (newAnalyzer: Partial) => void;
};
}
-const useAppState = create((set, _) => ({
+const useAppState = create((set) => ({
user: {
canvasInteractionEventTracker: new EventDetector(),
},
- visual: {
+ visual: VISUAL_REGISTRY.grid,
+ appearance: {
palette: COLOR_PALETTE.THREE_COOL_TO_WARM,
+ colorBackground: true,
+ paletteTrackEnergy: false,
+ showUI: true,
+ },
+ camera: {
+ mode: "ORBIT_CONTROLS",
+ autoOrbitAfterSleepMs: 10000,
+ },
+ analyzers: {
+ fft: {
+ octaveBandMode: 2,
+ energyMeasure: "bass",
+ },
},
- visualSourceData: {
- x: new Float32Array(121).fill(0),
- y: new Float32Array(121).fill(0),
+ mode: APPLICATION_MODE.WAVE_FORM,
+ mappers: {
+ textureMapper: new TextureMapper(),
+ coordinateMapperWaveform: new CoordinateMapper_WaveformSuperposition(),
+ coordinateMapperNoise: new CoordinateMapper_Noise(),
+ coordinateMapperData: new CoordinateMapper_Data(),
+ energyTracker: new EnergyTracker(0),
+ motionMapper: new MotionMapper_Noise(2.0, 0.5),
+ },
+ audio: {
+ source: getPlatformSupportedAudioSources()[0],
},
- energyInfo: { current: 0 },
actions: {
+ setVisual: (newVisualId: TVisualId) =>
+ set((state) => {
+ const newVisual = VISUAL_REGISTRY[newVisualId];
+ return [...newVisual.supportedApplicationModes].includes(state.mode)
+ ? {
+ visual: newVisual,
+ // mappers values whenever the visual changes
+ mappers: {
+ ...state.mappers,
+ textureMapper: new TextureMapper(),
+ },
+ }
+ : {};
+ }),
noteCanvasInteraction: () =>
set((state) => {
state.user.canvasInteractionEventTracker.addEvent();
@@ -52,38 +139,100 @@ const useAppState = create((set, _) => ({
},
};
}),
- setPalette: (newPalette: ColorPaletteType) =>
- set((_) => {
+ setAppearance: (newAppearance: Partial) =>
+ set((state) => {
return {
- visual: { palette: newPalette },
+ appearance: {
+ ...state.appearance,
+ ...newAppearance,
+ },
};
}),
nextPalette: () =>
set((state) => {
const currIdx =
- AVAILABLE_COLOR_PALETTES.indexOf(state.visual.palette) ?? 0;
+ AVAILABLE_COLOR_PALETTES.indexOf(state.appearance.palette) ?? 0;
const nextIdx = (currIdx + 1) % AVAILABLE_COLOR_PALETTES.length;
return {
- visual: { palette: AVAILABLE_COLOR_PALETTES[nextIdx] },
+ appearance: {
+ ...state.appearance,
+ palette: AVAILABLE_COLOR_PALETTES[nextIdx],
+ },
};
}),
- resizeVisualSourceData: (newSize: number) =>
- set((_) => {
+ setMode: (newMode: TApplicationMode) =>
+ set((state) => {
return {
- visualSourceData: {
- x: new Float32Array(newSize).fill(0),
- y: new Float32Array(newSize).fill(0),
+ mode: newMode,
+ ...(![...state.visual.supportedApplicationModes].includes(newMode)
+ ? {
+ visual: Object.values(VISUAL_REGISTRY).find((v) =>
+ [...v.supportedApplicationModes].includes(newMode),
+ ),
+ }
+ : {}),
+ mappers: {
+ ...state.mappers,
+ energyTracker:
+ newMode === APPLICATION_MODE.AUDIO
+ ? new EnergyTracker(0)
+ : NoOpScalarTracker,
+ },
+ appearance: {
+ ...state.appearance,
+ // Reset paletteTrackEnergy whenever the mode changes
+ paletteTrackEnergy: newMode === APPLICATION_MODE.AUDIO,
+ // Set default appearance settings for audio scope mode
+ ...(newMode === APPLICATION_MODE.AUDIO_SCOPE
+ ? {
+ palette: "rainbow",
+ }
+ : {}),
},
};
}),
+ setMappers: (newMappers: Partial) =>
+ set((state) => ({
+ mappers: {
+ ...state.mappers,
+ ...newMappers,
+ },
+ })),
+ setAudio: (newAudio: Partial) =>
+ set((state) => ({
+ audio: {
+ ...state.audio,
+ ...newAudio,
+ },
+ })),
+ setCamera: (newCamera: Partial) =>
+ set((state) => ({
+ camera: {
+ ...state.camera,
+ ...newCamera,
+ },
+ })),
+ setAnalyzerFFT: (newAnalyzer: Partial) =>
+ set((state) => ({
+ analyzers: {
+ ...state.analyzers,
+ fft: {
+ ...state.analyzers.fft,
+ ...newAnalyzer,
+ },
+ },
+ })),
},
}));
+export const useAnalyzerFFT = () => useAppState((state) => state.analyzers.fft);
+export const useCameraState = () => useAppState((state) => state.camera);
+export const useVisual = () => useAppState((state) => state.visual);
+export const useAppearance = () => useAppState((state) => state.appearance);
+export const usePalette = () =>
+ useAppState((state) => state.appearance.palette);
+export const useMode = () => useAppState((state) => state.mode);
export const useUser = () => useAppState((state) => state.user);
-export const usePalette = () => useAppState((state) => state.visual.palette);
-export const useVisualSourceDataX = () =>
- useAppState((state) => state.visualSourceData.x);
-export const useVisualSourceDataY = () =>
- useAppState((state) => state.visualSourceData.y);
-export const useEnergyInfo = () => useAppState((state) => state.energyInfo);
+export const useMappers = () => useAppState((state) => state.mappers);
+export const useAudio = () => useAppState((state) => state.audio);
export const useAppStateActions = () => useAppState((state) => state.actions);
diff --git a/app/src/lib/applicationModes.ts b/app/src/lib/applicationModes.ts
index e57978f0..832d8bae 100644
--- a/app/src/lib/applicationModes.ts
+++ b/app/src/lib/applicationModes.ts
@@ -7,9 +7,9 @@ export const APPLICATION_MODE = {
} as const;
type ObjectValues = T[keyof T];
-export type ApplicationMode = ObjectValues;
+export type TApplicationMode = ObjectValues;
-export const isAudioMode = (mode: ApplicationMode) => {
+export const isAudioMode = (mode: TApplicationMode) => {
switch (mode) {
case APPLICATION_MODE.WAVE_FORM:
case APPLICATION_MODE.NOISE:
@@ -23,19 +23,18 @@ export const isAudioMode = (mode: ApplicationMode) => {
}
};
-export const getPlatformSupportedApplicationModes = (): ApplicationMode[] => {
+export const getPlatformSupportedApplicationModes = () => {
return [
APPLICATION_MODE.WAVE_FORM,
APPLICATION_MODE.NOISE,
APPLICATION_MODE.AUDIO,
+ APPLICATION_MODE.PARTICLE_NOISE,
/* Disabled until bugs can be resolved */
- // APPLICATION_MODE.AUDIO_SCOPE,
- /* Disabled until IMotionMappers & ICoordinateMappers are more compatible */
- // APPLICATION_MODE.PARTICLE_NOISE,
+ APPLICATION_MODE.AUDIO_SCOPE,
];
};
-export const isCameraMode = (mode: ApplicationMode) => {
+export const isCameraMode = (mode: TApplicationMode) => {
switch (mode) {
case APPLICATION_MODE.WAVE_FORM:
case APPLICATION_MODE.NOISE:
diff --git a/app/src/lib/mappers/coordinateMappers/common.ts b/app/src/lib/mappers/coordinateMappers/common.ts
index 97ed6daf..04e295cc 100644
--- a/app/src/lib/mappers/coordinateMappers/common.ts
+++ b/app/src/lib/mappers/coordinateMappers/common.ts
@@ -112,7 +112,7 @@ export interface ICoordinateMapper {
* A base class for coordinate mapper implementations.
*/
export abstract class CoordinateMapperBase implements ICoordinateMapper {
- public readonly amplitude: number;
+ public amplitude: number;
/**
*
diff --git a/app/src/lib/mappers/coordinateMappers/data/controls.tsx b/app/src/lib/mappers/coordinateMappers/data/controls.tsx
new file mode 100644
index 00000000..62d97215
--- /dev/null
+++ b/app/src/lib/mappers/coordinateMappers/data/controls.tsx
@@ -0,0 +1,3 @@
+export default () => {
+ return null;
+};
diff --git a/app/src/lib/mappers/coordinateMappers/data/index.tsx b/app/src/lib/mappers/coordinateMappers/data/index.tsx
new file mode 100644
index 00000000..52e56b0b
--- /dev/null
+++ b/app/src/lib/mappers/coordinateMappers/data/index.tsx
@@ -0,0 +1,12 @@
+import ControlsComponent from "./controls";
+import { useActions, useInstance, usePresets } from "./store";
+
+export default {
+ id: "data",
+ ControlsComponent,
+ hooks: {
+ useActions,
+ useInstance,
+ usePresets,
+ },
+} as const;
diff --git a/app/src/lib/mappers/coordinateMappers/data.ts b/app/src/lib/mappers/coordinateMappers/data/mapper.ts
similarity index 70%
rename from app/src/lib/mappers/coordinateMappers/data.ts
rename to app/src/lib/mappers/coordinateMappers/data/mapper.ts
index c5640db2..492d059a 100644
--- a/app/src/lib/mappers/coordinateMappers/data.ts
+++ b/app/src/lib/mappers/coordinateMappers/data/mapper.ts
@@ -5,21 +5,47 @@ import {
HALF_DIAGONAL_UNIT_SQUARE,
} from "@/lib/mappers/coordinateMappers/common";
+export type TCoordinateMapper_DataParams = {
+ amplitude: number;
+ size: number;
+};
/**
* Maps input coordinates to output values based on pre-existing 1D data.
* Supports interpolation and anti-aliasing.
*/
export class CoordinateMapper_Data extends CoordinateMapperBase {
+ public static get PRESETS() {
+ return {
+ DEFAULT: {
+ amplitude: 1.0,
+ size: 121,
+ },
+ };
+ }
+ public clone(params: Partial) {
+ return new CoordinateMapper_Data({
+ ...this._params,
+ ...params,
+ });
+ }
+ private _params: TCoordinateMapper_DataParams;
+ public get params(): TCoordinateMapper_DataParams {
+ return this._params;
+ }
public data: Float32Array;
/**
*
* @param amplitude - the maximum amplitude of the scaled output.
- * @param data - the pre-existing 1D data from which to interpolate values.
+ * @param data - the size of 1D data from which to interpolate values.
*/
- constructor(amplitude = 1.0, data: Float32Array) {
- super(amplitude);
- this.data = data;
+ constructor(
+ params: TCoordinateMapper_DataParams = CoordinateMapper_Data.PRESETS
+ .DEFAULT,
+ ) {
+ super(params.amplitude);
+ this._params = params;
+ this.data = new Float32Array(params.size).fill(0);
}
private interpolateValueForNormalizedCoord(normalizedCoord: number): number {
diff --git a/app/src/lib/mappers/coordinateMappers/data/store.ts b/app/src/lib/mappers/coordinateMappers/data/store.ts
new file mode 100644
index 00000000..b8cd12eb
--- /dev/null
+++ b/app/src/lib/mappers/coordinateMappers/data/store.ts
@@ -0,0 +1,15 @@
+import { createConfigurableInstance } from "@/lib/storeHelpers";
+
+import {
+ CoordinateMapper_Data,
+ type TCoordinateMapper_DataParams,
+} from "./mapper";
+
+const { useActions, useInstance, usePresets } = createConfigurableInstance<
+ CoordinateMapper_Data,
+ TCoordinateMapper_DataParams
+>(new CoordinateMapper_Data(), {
+ default: CoordinateMapper_Data.PRESETS.DEFAULT,
+});
+
+export { useActions, useInstance, usePresets };
diff --git a/app/src/lib/mappers/coordinateMappers/noise/controls.tsx b/app/src/lib/mappers/coordinateMappers/noise/controls.tsx
new file mode 100644
index 00000000..4ce1bb36
--- /dev/null
+++ b/app/src/lib/mappers/coordinateMappers/noise/controls.tsx
@@ -0,0 +1,75 @@
+import { ValueLabel } from "@/components/controls/common";
+import { Button } from "@/components/ui/button";
+import { Label } from "@/components/ui/label";
+import { Slider } from "@/components/ui/slider";
+
+import { useActions, useInstance, usePresets } from "./store";
+
+export default () => {
+ const mapper = useInstance();
+ const { active: activePreset, options: presetOptions } = usePresets();
+ const { setPreset, setParams } = useActions();
+ const params = mapper.params;
+
+ return (
+
+
+
+ {[...Object.keys(presetOptions), "custom"].map((p) => (
+
+ ))}
+
+ {!activePreset && (
+ <>
+
+
setParams({ amplitude: e[0] })}
+ />
+
+ setParams({ spatialScale: e[0] })}
+ />
+
+ ({ timeScale: e[0] })}
+ />
+
+ ({ nIterations: e[0] })}
+ />
+ >
+ )}
+
+ );
+};
diff --git a/app/src/lib/mappers/coordinateMappers/noise/index.tsx b/app/src/lib/mappers/coordinateMappers/noise/index.tsx
new file mode 100644
index 00000000..71cae360
--- /dev/null
+++ b/app/src/lib/mappers/coordinateMappers/noise/index.tsx
@@ -0,0 +1,12 @@
+import ControlsComponent from "./controls";
+import { useActions, useInstance, usePresets } from "./store";
+
+export default {
+ id: "noise",
+ ControlsComponent,
+ hooks: {
+ useActions,
+ useInstance,
+ usePresets,
+ },
+} as const;
diff --git a/app/src/lib/mappers/coordinateMappers/noise.ts b/app/src/lib/mappers/coordinateMappers/noise/mapper.ts
similarity index 57%
rename from app/src/lib/mappers/coordinateMappers/noise.ts
rename to app/src/lib/mappers/coordinateMappers/noise/mapper.ts
index c96b88d5..9d3ab7db 100644
--- a/app/src/lib/mappers/coordinateMappers/noise.ts
+++ b/app/src/lib/mappers/coordinateMappers/noise/mapper.ts
@@ -11,34 +11,52 @@ import {
type NoiseFunction4D,
} from "simplex-noise";
+export type TCoordinateMapper_NoiseParams = {
+ amplitude: number;
+ spatialScale: number;
+ timeScale: number;
+ nIterations: number;
+ persistence: number;
+};
/**
* Maps input coordinates to output values based on the noise functions.
*/
export class CoordinateMapper_Noise extends CoordinateMapperBase {
+ public static get PRESETS() {
+ return {
+ DEFAULT: {
+ amplitude: 1.0,
+ spatialScale: 2.0,
+ timeScale: 0.5,
+ nIterations: 10,
+ persistence: 0.5,
+ },
+ };
+ }
+ public clone(params: Partial) {
+ return new CoordinateMapper_Noise({
+ ...this._params,
+ ...params,
+ });
+ }
+ private _params: TCoordinateMapper_NoiseParams;
+ public get params(): TCoordinateMapper_NoiseParams {
+ return this._params;
+ }
private readonly noise2D: NoiseFunction2D;
private readonly noise3D: NoiseFunction3D;
private readonly noise4D: NoiseFunction4D;
- private readonly spatialScale: number;
- private readonly timeScale: number;
- private readonly nIterations: number;
- private readonly persistence: number;
/**
*
* @param amplitude - the maximum amplitude of the scaled output.
*/
constructor(
- amplitude = 1.0,
- spatialScale = 1.0,
- timeScale = 1.0,
- nIterations = 1,
- persistence = 0.5,
+ params: TCoordinateMapper_NoiseParams = CoordinateMapper_Noise.PRESETS
+ .DEFAULT,
) {
- super(amplitude);
- this.spatialScale = spatialScale;
- this.timeScale = timeScale;
- this.nIterations = nIterations;
- this.persistence = persistence;
+ super(params.amplitude);
+ this._params = params;
this.noise2D = createNoise2D();
this.noise3D = createNoise3D();
this.noise4D = createNoise4D();
@@ -48,28 +66,28 @@ export class CoordinateMapper_Noise extends CoordinateMapperBase {
let noise = 0,
maxAmp = 0,
amp = this.amplitude,
- spatialScale = this.spatialScale;
- const timeScale = this.timeScale;
+ spatialScale = this._params.spatialScale;
+ const timeScale = this._params.timeScale;
- for (let i = 0; i < this.nIterations; i++) {
+ for (let i = 0; i < this._params.nIterations; i++) {
noise +=
amp * this.noise2D(xNorm * spatialScale, elapsedTimeSec * timeScale);
maxAmp += amp;
- amp *= this.persistence;
+ amp *= this._params.persistence;
spatialScale *= 2;
}
- return this.nIterations > 1 ? noise / maxAmp : noise;
+ return this._params.nIterations > 1 ? noise / maxAmp : noise;
}
public map_2D(xNorm: number, yNorm: number, elapsedTimeSec = 0.0): number {
let noise = 0,
maxAmp = 0,
amp = this.amplitude,
- spatialScale = this.spatialScale;
- const timeScale = this.timeScale;
+ spatialScale = this._params.spatialScale;
+ const timeScale = this._params.timeScale;
- for (let i = 0; i < this.nIterations; i++) {
+ for (let i = 0; i < this._params.nIterations; i++) {
noise +=
amp *
this.noise3D(
@@ -78,11 +96,11 @@ export class CoordinateMapper_Noise extends CoordinateMapperBase {
elapsedTimeSec * timeScale,
);
maxAmp += amp;
- amp *= this.persistence;
+ amp *= this._params.persistence;
spatialScale *= 2;
}
- return this.nIterations > 1 ? noise / maxAmp : noise;
+ return this._params.nIterations > 1 ? noise / maxAmp : noise;
}
public map_3D(
@@ -94,10 +112,10 @@ export class CoordinateMapper_Noise extends CoordinateMapperBase {
let noise = 0,
maxAmp = 0,
amp = this.amplitude,
- spatialScale = this.spatialScale;
- const timeScale = this.timeScale;
+ spatialScale = this._params.spatialScale;
+ const timeScale = this._params.timeScale;
- for (let i = 0; i < this.nIterations; i++) {
+ for (let i = 0; i < this._params.nIterations; i++) {
noise +=
amp *
this.noise4D(
@@ -107,11 +125,11 @@ export class CoordinateMapper_Noise extends CoordinateMapperBase {
elapsedTimeSec * timeScale,
);
maxAmp += amp;
- amp *= this.persistence;
+ amp *= this._params.persistence;
spatialScale *= 2;
}
- return this.nIterations > 1 ? noise / maxAmp : noise;
+ return this._params.nIterations > 1 ? noise / maxAmp : noise;
}
public map_3DFaces(
diff --git a/app/src/lib/mappers/coordinateMappers/noise/store.ts b/app/src/lib/mappers/coordinateMappers/noise/store.ts
new file mode 100644
index 00000000..e691e722
--- /dev/null
+++ b/app/src/lib/mappers/coordinateMappers/noise/store.ts
@@ -0,0 +1,15 @@
+import { createConfigurableInstance } from "@/lib/storeHelpers";
+
+import {
+ CoordinateMapper_Noise,
+ type TCoordinateMapper_NoiseParams,
+} from "./mapper";
+
+const { useActions, useInstance, usePresets } = createConfigurableInstance<
+ CoordinateMapper_Noise,
+ TCoordinateMapper_NoiseParams
+>(new CoordinateMapper_Noise(), {
+ default: CoordinateMapper_Noise.PRESETS.DEFAULT,
+});
+
+export { useActions, useInstance, usePresets };
diff --git a/app/src/lib/mappers/coordinateMappers/registry.tsx b/app/src/lib/mappers/coordinateMappers/registry.tsx
new file mode 100644
index 00000000..0a0116ee
--- /dev/null
+++ b/app/src/lib/mappers/coordinateMappers/registry.tsx
@@ -0,0 +1,9 @@
+import Data from "./data";
+import Noise from "./noise";
+import Waveform from "./waveform";
+
+export const COORDINATE_MAPPER_REGISTRY = {
+ [Data.id]: Data,
+ [Noise.id]: Noise,
+ [Waveform.id]: Waveform,
+};
diff --git a/app/src/lib/mappers/coordinateMappers/waveform/controls.tsx b/app/src/lib/mappers/coordinateMappers/waveform/controls.tsx
new file mode 100644
index 00000000..08419625
--- /dev/null
+++ b/app/src/lib/mappers/coordinateMappers/waveform/controls.tsx
@@ -0,0 +1,87 @@
+import { ValueLabel } from "@/components/controls/common";
+import { Button } from "@/components/ui/button";
+import { Label } from "@/components/ui/label";
+import { Slider } from "@/components/ui/slider";
+import { Switch } from "@/components/ui/switch";
+
+import { useActions, useInstance, usePresets } from "./store";
+
+export default () => {
+ const mapper = useInstance();
+ const { active: activePreset, options: presetOptions } = usePresets();
+ const { setPreset, setParams } = useActions();
+ const params = mapper.params;
+
+ return (
+
+
+
+ {[...Object.keys(presetOptions), "custom"].map((p) => (
+
+ ))}
+
+ {!activePreset && (
+ <>
+
+
+ 1}
+ defaultChecked={params.waveformFrequenciesHz.length > 1}
+ onCheckedChange={(e) => {
+ setParams({
+ waveformFrequenciesHz: e ? [2.0, 10] : [2.0],
+ });
+ }}
+ />
+
+
+
setParams({ maxAmplitude: e[0] })}
+ />
+ {params.waveformFrequenciesHz.map((hz, i) => (
+
+
+
+ setParams({
+ waveformFrequenciesHz: params.waveformFrequenciesHz.map(
+ (v, j) => (i == j ? e[0] : v),
+ ),
+ })
+ }
+ />
+
+ ))}
+ >
+ )}
+
+ );
+};
diff --git a/app/src/lib/mappers/coordinateMappers/waveform/index.tsx b/app/src/lib/mappers/coordinateMappers/waveform/index.tsx
new file mode 100644
index 00000000..2c0e5ac4
--- /dev/null
+++ b/app/src/lib/mappers/coordinateMappers/waveform/index.tsx
@@ -0,0 +1,12 @@
+import ControlsComponent from "./controls";
+import { useActions, useInstance, usePresets } from "./store";
+
+export default {
+ id: "waveform",
+ ControlsComponent,
+ hooks: {
+ useActions,
+ useInstance,
+ usePresets,
+ },
+} as const;
diff --git a/app/src/lib/mappers/coordinateMappers/waveform.ts b/app/src/lib/mappers/coordinateMappers/waveform/mapper.ts
similarity index 60%
rename from app/src/lib/mappers/coordinateMappers/waveform.ts
rename to app/src/lib/mappers/coordinateMappers/waveform/mapper.ts
index 74c14033..00aa375c 100644
--- a/app/src/lib/mappers/coordinateMappers/waveform.ts
+++ b/app/src/lib/mappers/coordinateMappers/waveform/mapper.ts
@@ -12,6 +12,7 @@ import {
* Maps input coordinates to output values based on a time varying waveform.
*/
export class CoordinateMapper_Waveform extends CoordinateMapperBase {
+ public readonly frequencyHz: number;
protected periodSec: number;
protected b: number;
@@ -22,6 +23,7 @@ export class CoordinateMapper_Waveform extends CoordinateMapperBase {
*/
constructor(amplitude = 1.0, frequencyHz: number) {
super(amplitude);
+ this.frequencyHz = frequencyHz;
this.periodSec = 1 / frequencyHz;
this.b = TWO_PI / this.periodSec;
}
@@ -70,14 +72,48 @@ export class CoordinateMapper_Waveform extends CoordinateMapperBase {
}
}
+export type TSuperPositionParams = {
+ waveformFrequenciesHz: number[];
+ maxAmplitude: number;
+ amplitudeSplitRatio: number;
+};
/**
* Maps input coordinates to output values based on the superposition of multiple time varying waveforms.
*/
export class CoordinateMapper_WaveformSuperposition
implements ICoordinateMapper
{
+ public static get PRESETS() {
+ return {
+ DEFAULT: {
+ waveformFrequenciesHz: [2.0],
+ maxAmplitude: 1.0,
+ amplitudeSplitRatio: 0.75,
+ },
+ DOUBLE: {
+ waveformFrequenciesHz: [2.0, 10.0],
+ maxAmplitude: 1.0,
+ amplitudeSplitRatio: 0.75,
+ },
+ CUSTOM: {},
+ };
+ }
+
+ public clone(params: Partial) {
+ return new CoordinateMapper_WaveformSuperposition({
+ ...this._params,
+ ...params,
+ });
+ }
+
private mappers: CoordinateMapper_Waveform[];
- public readonly amplitude: number;
+ private _params: TSuperPositionParams;
+ public get params(): TSuperPositionParams {
+ return this._params;
+ }
+ public get amplitude() {
+ return this.params.maxAmplitude;
+ }
/**
* @param waveformFrequenciesHz - the frequency (in hz) for each of the time varying waveforms
@@ -85,24 +121,32 @@ export class CoordinateMapper_WaveformSuperposition
* @param amplitudeSplitRatio - the recursive split ratio controlling how amplitude is divided among the various waveforms
*/
constructor(
- waveformFrequenciesHz: number[],
- maxAmplitude = 1.0,
- amplitudeSplitRatio = 0.75,
+ params: TSuperPositionParams = CoordinateMapper_WaveformSuperposition
+ .PRESETS.DEFAULT,
) {
- this.amplitude = maxAmplitude;
- this.mappers = [];
- for (let i = 0; i < waveformFrequenciesHz.length; i++) {
- // Split the total amplitude among the various waves
- const amplitude =
- i >= waveformFrequenciesHz.length - 1
- ? maxAmplitude
- : amplitudeSplitRatio * maxAmplitude;
- maxAmplitude -= amplitude;
+ this._params = params;
+ this.mappers = CoordinateMapper_WaveformSuperposition.genMappersForParams(
+ this.params,
+ );
+ }
- this.mappers.push(
- new CoordinateMapper_Waveform(amplitude, waveformFrequenciesHz[i]),
- );
- }
+ private static genMappersForParams(params: TSuperPositionParams) {
+ let maxAmplitude = params.maxAmplitude;
+ return Array.from({ length: params.waveformFrequenciesHz.length }).map(
+ (_, i) => {
+ // Split the total amplitude among the various waves
+ // const amplitude = i > 0 ? params.amplitudeSplitRatio * maxAmplitude : maxAmplitude;
+ const amplitude =
+ i >= params.waveformFrequenciesHz.length - 1
+ ? maxAmplitude
+ : params.amplitudeSplitRatio * maxAmplitude;
+ maxAmplitude -= amplitude;
+ return new CoordinateMapper_Waveform(
+ amplitude,
+ params.waveformFrequenciesHz[i],
+ );
+ },
+ );
}
public map(
diff --git a/app/src/lib/mappers/coordinateMappers/waveform/store.ts b/app/src/lib/mappers/coordinateMappers/waveform/store.ts
new file mode 100644
index 00000000..1cf95a17
--- /dev/null
+++ b/app/src/lib/mappers/coordinateMappers/waveform/store.ts
@@ -0,0 +1,15 @@
+import { createConfigurableInstance } from "@/lib/storeHelpers";
+
+import {
+ CoordinateMapper_WaveformSuperposition,
+ type TSuperPositionParams,
+} from "./mapper";
+
+const { useActions, useInstance, usePresets } = createConfigurableInstance<
+ CoordinateMapper_WaveformSuperposition,
+ TSuperPositionParams
+>(new CoordinateMapper_WaveformSuperposition(), {
+ default: CoordinateMapper_WaveformSuperposition.PRESETS.DEFAULT,
+});
+
+export { useActions, useInstance, usePresets };
diff --git a/app/src/lib/mappers/textureMappers/textureMapper.ts b/app/src/lib/mappers/textureMappers/textureMapper.ts
new file mode 100644
index 00000000..a0bb64cd
--- /dev/null
+++ b/app/src/lib/mappers/textureMappers/textureMapper.ts
@@ -0,0 +1,88 @@
+import { DataTexture, RGBAFormat } from "three";
+
+export type TTextureMapperParams = {
+ size: number;
+};
+export class TextureMapper {
+ public static get PRESETS() {
+ return {
+ DEFAULT: {
+ size: 512,
+ },
+ };
+ }
+ public clone(params: Partial) {
+ return new TextureMapper({
+ ...this._params,
+ ...params,
+ });
+ }
+ private _params: TTextureMapperParams;
+ public get params(): TTextureMapperParams {
+ return {
+ ...this.params,
+ };
+ }
+ public samplesX: Float32Array;
+ public samplesY: Float32Array;
+ public maxAmplitude = 4.0;
+ private readonly M: number = 4;
+
+ constructor(params: TTextureMapperParams = TextureMapper.PRESETS.DEFAULT) {
+ this._params = params;
+ this.samplesX = new Float32Array(params.size).fill(0);
+ this.samplesY = new Float32Array(params.size).fill(0);
+ }
+
+ public updateParams(params: Partial): void {
+ this._params = {
+ ...this._params,
+ ...params,
+ };
+ this.samplesX = new Float32Array(this._params.size).fill(0);
+ this.samplesY = new Float32Array(this._params.size).fill(0);
+ }
+
+ public updateTextureData(data: Uint8Array): void {
+ const B = (1 << 16) - 1;
+ let j, x, y;
+ for (let i = 0; i < this.samplesX.length; i++) {
+ x = Math.max(
+ 0,
+ Math.min(
+ 2 * this.maxAmplitude,
+ 0.5 + (0.5 * this.samplesX[i]) / this.maxAmplitude,
+ ),
+ );
+ y = Math.max(
+ 0,
+ Math.min(
+ 2 * this.maxAmplitude,
+ 0.5 + (0.5 * this.samplesY[i]) / this.maxAmplitude,
+ ),
+ );
+
+ x = (x * B) | 0;
+ y = (y * B) | 0;
+ j = i * this.M;
+ data[j + 0] = x >> 8;
+ data[j + 1] = x & 0xff;
+ data[j + 2] = y >> 8;
+ data[j + 3] = y & 0xff;
+ }
+ }
+
+ public generateSupportedTextureAndData() {
+ const textureData = new Uint8Array(this.samplesX.length * this.M);
+ const tex = new DataTexture(
+ textureData,
+ this.samplesX.length,
+ 1,
+ RGBAFormat,
+ );
+ return {
+ tex: tex,
+ textureData: textureData,
+ };
+ }
+}
diff --git a/app/src/lib/mappers/valueTracker/common.ts b/app/src/lib/mappers/valueTracker/common.ts
index 7f1741da..0daac11e 100644
--- a/app/src/lib/mappers/valueTracker/common.ts
+++ b/app/src/lib/mappers/valueTracker/common.ts
@@ -2,5 +2,12 @@
* Tracks scalar values
*/
export interface IScalarTracker {
- getNormalizedValue(): number;
+ set(value: number): void;
+ get(): number;
}
+
+export const NoOpScalarTracker: IScalarTracker = {
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
+ set: () => {},
+ get: () => 0,
+};
diff --git a/app/src/lib/mappers/valueTracker/energyTracker.ts b/app/src/lib/mappers/valueTracker/energyTracker.ts
index da85a768..64121b64 100644
--- a/app/src/lib/mappers/valueTracker/energyTracker.ts
+++ b/app/src/lib/mappers/valueTracker/energyTracker.ts
@@ -1,12 +1,17 @@
import { type IScalarTracker } from "@/lib/mappers/valueTracker/common";
export class EnergyTracker implements IScalarTracker {
- private readonly _energyInfo: { current: number };
+ private _energyInfo: number;
- constructor(energyInfo: { current: number }) {
+ constructor(energyInfo: number) {
this._energyInfo = energyInfo;
}
- public getNormalizedValue(): number {
- return this._energyInfo.current;
+
+ public set(value: number): void {
+ this._energyInfo = value;
+ }
+
+ public get(): number {
+ return this._energyInfo;
}
}
diff --git a/app/src/lib/storeHelpers.ts b/app/src/lib/storeHelpers.ts
new file mode 100644
index 00000000..888acfe4
--- /dev/null
+++ b/app/src/lib/storeHelpers.ts
@@ -0,0 +1,100 @@
+import { create } from "zustand";
+
+export function createConfigStore(
+ presets: { default: T } & Record,
+) {
+ const useStore = create<{
+ params: T;
+ presets: {
+ active: keyof typeof presets | undefined;
+ options: typeof presets;
+ };
+ actions: {
+ setParams: (newParams: Partial) => void;
+ setPreset: (preset: keyof typeof presets | undefined) => void;
+ };
+ }>()((set) => ({
+ presets: {
+ active: "default",
+ options: presets,
+ },
+ params: {
+ ...presets.default,
+ },
+ actions: {
+ setParams: (newParams: Partial) =>
+ set((state) => {
+ return {
+ params: { ...state.params, ...newParams },
+ };
+ }),
+ setPreset: (preset: keyof typeof presets | undefined) =>
+ set((state) => {
+ return {
+ presets: { ...state.presets, active: preset },
+ ...(!!preset && {
+ params: {
+ ...state.params,
+ ...state.presets.options[preset],
+ },
+ }),
+ };
+ }),
+ },
+ }));
+ return {
+ useStore,
+ usePresets: () => useStore((state) => state.presets),
+ useParams: () => useStore((state) => state.params),
+ useActions: () => useStore((state) => state.actions),
+ };
+}
+
+export function createConfigurableInstance<
+ U extends {
+ params: T;
+ clone: (params: Partial) => U;
+ },
+ T,
+>(initialInstance: U, presets: { default: T } & Record) {
+ const useStore = create<{
+ instance: U;
+ presets: {
+ active: keyof typeof presets | undefined;
+ options: typeof presets;
+ };
+ actions: {
+ setParams: (newParams: Partial) => void;
+ setPreset: (preset: keyof typeof presets | undefined) => void;
+ };
+ }>()((set) => ({
+ presets: {
+ active: "default",
+ options: presets,
+ },
+ instance: initialInstance,
+ actions: {
+ setParams: (newParams: Partial) =>
+ set((state) => {
+ return {
+ instance: state.instance.clone(newParams),
+ };
+ }),
+ setPreset: (preset: keyof typeof presets | undefined) =>
+ set((state) => {
+ return {
+ presets: { ...state.presets, active: preset },
+ ...(!!preset && {
+ instance: state.instance.clone(state.presets.options[preset]),
+ }),
+ };
+ }),
+ },
+ }));
+ return {
+ useStore,
+ usePresets: () => useStore((state) => state.presets),
+ useInstance: () => useStore((state) => state.instance),
+ useActions: () => useStore((state) => state.actions),
+ };
+}
diff --git a/app/src/main.tsx b/app/src/main.tsx
index 7f99d7d9..7f63c9f4 100644
--- a/app/src/main.tsx
+++ b/app/src/main.tsx
@@ -5,16 +5,9 @@ import { createRoot } from "react-dom/client";
import "@/style/globals.css";
-import { CameraControlsContextProvider } from "@/context/cameraControls";
-import { ModeContextProvider } from "@/context/mode";
import { ThemeProvider } from "@/context/theme";
-import { VisualContextProvider } from "@/context/visual";
-import { AudioSourceContextProvider } from "./context/audioSource";
-import { FFTAnalyzerContextProvider } from "./context/fftAnalyzer";
-import { NoiseGeneratorContextProvider } from "./context/noiseGenerator";
import { SoundcloudContextProvider } from "./context/soundcloud";
-import { WaveGeneratorContextProvider } from "./context/waveGenerator";
const queryClient = new QueryClient();
@@ -22,23 +15,9 @@ createRoot(document.getElementById("root")!).render(
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
,
diff --git a/app/tsconfig.json b/app/tsconfig.json
index 4b500e90..924010e7 100644
--- a/app/tsconfig.json
+++ b/app/tsconfig.json
@@ -7,18 +7,12 @@
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"jsx": "react-jsx",
- "lib": [
- "DOM",
- "DOM.Iterable",
- "ESNext"
- ],
+ "lib": ["DOM", "DOM.Iterable", "ESNext"],
"module": "ESNext",
"moduleResolution": "Node",
"noEmit": true,
"paths": {
- "@/*": [
- "./src/*"
- ]
+ "@/*": ["./src/*"]
},
"resolveJsonModule": true,
"skipLibCheck": true,
@@ -26,22 +20,11 @@
"target": "ESNext",
"useDefineForClassFields": true
},
- "exclude": [
- "node_modules",
- "dist",
- "scratch",
- "vite.config.ts"
- ],
- "include": [
- "src",
- "**/*.ts",
- "**/*.tsx",
- "**/*.js",
- "**/*.cjs"
- ],
+ "exclude": ["node_modules", "dist", "scratch", "vite.config.ts"],
+ "include": ["src", "**/*.ts", "**/*.tsx", "**/*.js", "**/*.cjs"],
"references": [
{
"path": "./tsconfig.node.json"
}
]
-}
\ No newline at end of file
+}