diff --git a/app/src/components/canvas/AutoOrbitCamera.tsx b/app/src/components/canvas/AutoOrbitCamera.tsx new file mode 100644 index 00000000..de967c55 --- /dev/null +++ b/app/src/components/canvas/AutoOrbitCamera.tsx @@ -0,0 +1,120 @@ +import { useVisualContext } from "@/context/visual"; +import { useFrame, useThree } from "@react-three/fiber"; +import { Spherical, type Vector3 } from "three"; + +const setFromSphericalZUp = (vec: Vector3, s: Spherical) => { + const sinPhiRadius = Math.sin(s.phi) * s.radius; + vec.x = sinPhiRadius * Math.sin(s.theta); + vec.z = Math.cos(s.phi) * s.radius; + vec.y = sinPhiRadius * Math.cos(s.theta); + return vec; +}; + +const useSphericalLimits = () => { + const { visual } = useVisualContext(); + // r is the Radius + // theta is the equator angle + // phi is the polar angle + switch (visual) { + case "ribbons": + return { + rMin: 10, + rMax: 15, + rSpeed: 0.1, + thetaMin: Math.PI / 8, + thetaMax: 2 * Math.PI - Math.PI / 8, + thetaSpeed: 0.025, + phiMin: Math.PI / 3, + phiMax: Math.PI / 2.1, + phiSpeed: 0.25, + }; + case "sphere": + return { + rMin: 10, + rMax: 15, + rSpeed: 0.1, + thetaMin: 0, + thetaMax: 2 * Math.PI, + thetaSpeed: 0.025, + phiMin: Math.PI / 3, + phiMax: Math.PI / 2, + phiSpeed: 0.25, + }; + case "cube": + return { + rMin: 12, + rMax: 20, + rSpeed: 0.1, + thetaMin: 0, + thetaMax: 2 * Math.PI, + thetaSpeed: 0.025, + phiMin: Math.PI / 4, + phiMax: Math.PI / 2, + phiSpeed: 0.25, + }; + case "diffusedRing": + return { + rMin: 10, + rMax: 18, + rSpeed: 0.1, + thetaMin: 0, + thetaMax: 2 * Math.PI, + thetaSpeed: 0.025, + phiMin: Math.PI / 8, + phiMax: Math.PI / 2.25, + phiSpeed: 0.25, + }; + case "boxes": + case "dna": + case "grid": + return { + rMin: 15, + rMax: 22, + rSpeed: 0.1, + thetaMin: 0, + thetaMax: 2 * Math.PI, + thetaSpeed: 0.025, + phiMin: Math.PI / 3, + phiMax: Math.PI / 2, + phiSpeed: 0.25, + }; + default: + return visual satisfies never; + } +}; + +export const AutoOrbitCameraControls = () => { + const camera = useThree((state) => state.camera); + // r is the Radius + // theta is the equator angle + // phi is the polar angle + const { + rMin, + rMax, + rSpeed, + thetaMin, + thetaMax, + thetaSpeed, + phiMin, + phiMax, + phiSpeed, + } = useSphericalLimits(); + const target = new Spherical(); + + useFrame(({ clock }) => { + const t = clock.elapsedTime; + + const rAlpha = 0.5 * (1 + Math.sin(t * rSpeed)); + const r = rMin + rAlpha * (rMax - rMin); + + const thetaAlpha = 0.5 * (1 + Math.cos(t * thetaSpeed)); + const theta = thetaMin + thetaAlpha * (thetaMax - thetaMin); + + const phiAlpha = 0.5 * (1 + Math.cos(t * phiSpeed)); + const phi = phiMin + phiAlpha * (phiMax - phiMin); + + setFromSphericalZUp(camera.position, target.set(r, phi, theta)); + camera.lookAt(0, 0, 0); + }); + return null; +}; diff --git a/app/src/components/canvas/Visual3D.tsx b/app/src/components/canvas/Visual3D.tsx index b234c239..610bd2ef 100644 --- a/app/src/components/canvas/Visual3D.tsx +++ b/app/src/components/canvas/Visual3D.tsx @@ -12,8 +12,9 @@ import { useVisualContext } from "@/context/visual"; import { APPLICATION_MODE } from "@/lib/applicationModes"; import { useUser } from "@/lib/appState"; import { OrbitControls } from "@react-three/drei"; -import { Canvas, useFrame, useThree } from "@react-three/fiber"; +import { Canvas, useFrame } from "@react-three/fiber"; +import { AutoOrbitCameraControls } from "./AutoOrbitCamera"; import { PaletteTracker } from "./paletteTracker"; const VisualizerComponent = ({ @@ -36,32 +37,6 @@ const VisualizerComponent = ({ } }; -const AutoOrbitCameraControls = () => { - const camera = useThree((state) => state.camera); - const [rMin, rMax, rSpeed] = [15, 22, 0.1]; - const thetaSpeed = 0.025; - const [polarMin, polarMax, polarSpeed] = [Math.PI / 3, Math.PI / 2, 0.25]; - useFrame(({ clock }) => { - const t = clock.elapsedTime; - - // r is the Radius - // theta is the horizontal angle from the X axis - // polar is the vertical angle from the Z axis - const rAlpha = 0.5 * (1 + Math.sin(t * rSpeed)); - const r = rMin + rAlpha * (rMax - rMin); - - const thetaAlpha = 0.5 * (1 + Math.cos(t * thetaSpeed)); - const theta = thetaAlpha * (2 * Math.PI); - const polarAlpha = 0.5 * (1 + Math.cos(t * polarSpeed)); - const polar = polarMin + polarAlpha * (polarMax - polarMin); - camera.position.x = r * Math.sin(polar) * Math.cos(theta); - camera.position.y = r * Math.sin(polar) * Math.sin(theta); - camera.position.z = r * Math.cos(polar); - camera.lookAt(0, 0, 0); - }); - return null; -}; - const CameraControls = () => { const { mode, autoOrbitAfterSleepMs } = useCameraControlsContext(); const { setMode } = useCameraControlsContextSetters(); diff --git a/app/src/components/controls/visualSettingsSheet.tsx b/app/src/components/controls/visualSettingsSheet.tsx index f073fefe..10769081 100644 --- a/app/src/components/controls/visualSettingsSheet.tsx +++ b/app/src/components/controls/visualSettingsSheet.tsx @@ -72,6 +72,7 @@ const VisualSettingsControls = () => { return SphereVisualSettingsControls(); case "diffusedRing": return DiffusedRingVisualSettingsControls(); + case "ribbons": case "dna": case "boxes": return null; diff --git a/app/src/components/controls/visualsDock.tsx b/app/src/components/controls/visualsDock.tsx index eb57bfc4..435199df 100644 --- a/app/src/components/controls/visualsDock.tsx +++ b/app/src/components/controls/visualsDock.tsx @@ -4,7 +4,15 @@ import { type VisualType, } from "@/components/visualizers/common"; import { useVisualContext, useVisualContextSetters } from "@/context/visual"; -import { Box, Boxes, CircleDashed, Dna, Globe, Grid3x3 } from "lucide-react"; +import { + Box, + Boxes, + CircleDashed, + Dna, + Globe, + Grid3x3, + Ribbon, +} from "lucide-react"; import { Dock, DockItem, DockNav } from "./dock"; @@ -22,6 +30,8 @@ const VisualIcon = ({ visual }: { visual: VisualType }) => { return ; case "boxes": return ; + case "ribbons": + return ; default: return visual satisfies never; } diff --git a/app/src/components/visualizers/common.ts b/app/src/components/visualizers/common.ts index 4a7b4660..afaff8d6 100644 --- a/app/src/components/visualizers/common.ts +++ b/app/src/components/visualizers/common.ts @@ -19,6 +19,7 @@ export const AVAILABLE_VISUALS = [ "diffusedRing", "dna", "boxes", + "ribbons", // "stencil", // "swarm", ] as const; diff --git a/app/src/components/visualizers/dna/multi.tsx b/app/src/components/visualizers/dna/multi.tsx index 69c84615..ce413d45 100644 --- a/app/src/components/visualizers/dna/multi.tsx +++ b/app/src/components/visualizers/dna/multi.tsx @@ -15,7 +15,7 @@ const MultiStrand = (props: BaseDoubleHelixProps) => { const strandCount = strandRefs.length; const bounds = 15; - const strandPositions = Array.from({ length: strandCount }).map((x, i) => { + const strandPositions = Array.from({ length: strandCount }).map((_, i) => { return new Vector3() .fromArray( Array.from({ length: 3 }).map( diff --git a/app/src/components/visualizers/ribbons/base.tsx b/app/src/components/visualizers/ribbons/base.tsx new file mode 100644 index 00000000..f3852af5 --- /dev/null +++ b/app/src/components/visualizers/ribbons/base.tsx @@ -0,0 +1,156 @@ +import { useMemo, useRef } from "react"; +import { usePalette } from "@/lib/appState"; +import { + COORDINATE_TYPE, + type ICoordinateMapper, +} from "@/lib/mappers/coordinateMappers/common"; +import { ColorPalette } from "@/lib/palettes"; +import { Plane } from "@react-three/drei"; +import { useFrame } from "@react-three/fiber"; +import { DoubleSide, Vector3, type Mesh } from "three"; + +const BaseRibbons = ({ + coordinateMapper, + ribbonWidth = 1, + ribbonHeight = 10, + ribbonWidthSegments = 1, + ribbonHeightSegments = 64, + zScale = 2, +}: { + coordinateMapper: ICoordinateMapper; + ribbonWidth?: number; + ribbonHeight?: number; + ribbonWidthSegments?: number; + ribbonHeightSegments?: number; + zScale?: number; +}) => { + const ribbonRefs = [ + useRef(null!), + useRef(null!), + useRef(null!), + useRef(null!), + useRef(null!), + ]; + const ribbonCount = ribbonRefs.length; + + const gridHalfWidth = (ribbonWidth * ribbonCount) / 2; + const gridHalfHeight = ribbonHeight / 2; + const ribbonPositions = Array.from({ length: ribbonCount }).map( + (_, i) => + new Vector3(gridHalfWidth - ribbonWidth * i - ribbonWidth / 2, 0, 0), + ); + const palette = usePalette(); + const colorPalette = ColorPalette.getPalette(palette); + // const lut = ColorPalette.getPalette(palette).buildLut(); + + const material = useMemo(() => { + return ( + + ); + }, [colorPalette]); + + useFrame(({ clock }) => { + //in ms + const elapsedTimeSec = clock.getElapsedTime(); + + let w, h, vIdx, normX, normY, z, alpha; + ribbonRefs.forEach((ribbonRef, ribbonIdx) => { + if (!ribbonRef.current) { + return; + } + + const positionsBuffer = ribbonRef.current.geometry.attributes.position; + for (h = 0; h <= ribbonHeightSegments; h++) { + alpha = + 1 - + Math.abs(h - ribbonHeightSegments / 2) / (ribbonHeightSegments / 2); + for (w = 0; w <= ribbonWidthSegments; w++) { + normX = (ribbonIdx + 0.5) / ribbonCount; + normY = (h + 0.5) / ribbonHeightSegments; + vIdx = h * (ribbonWidthSegments + 1) + w; + z = + zScale * + coordinateMapper.map( + COORDINATE_TYPE.CARTESIAN_2D, + normX, + normY, + 0, + elapsedTimeSec, + ); + + positionsBuffer.setZ(vIdx, z * alpha); + } + } + positionsBuffer.needsUpdate = true; + }); + }); + + const planeSize = 250; + return ( + <> + + + + {material} + + + {material} + + + {material} + + + {material} + + {ribbonRefs.map((ref, i) => ( + + {material} + + ))} + + ); +}; + +export default BaseRibbons; diff --git a/app/src/components/visualizers/ribbons/reactive.tsx b/app/src/components/visualizers/ribbons/reactive.tsx new file mode 100644 index 00000000..acd566e4 --- /dev/null +++ b/app/src/components/visualizers/ribbons/reactive.tsx @@ -0,0 +1,13 @@ +import { type VisualProps } from "@/components/visualizers/common"; + +import BaseRibbons from "./base"; + +const RibbonsVisual = ({ coordinateMapper }: VisualProps) => { + return ( + <> + + + ); +}; + +export default RibbonsVisual; diff --git a/app/src/context/visual.tsx b/app/src/context/visual.tsx index 2600cdd1..483e51cc 100644 --- a/app/src/context/visual.tsx +++ b/app/src/context/visual.tsx @@ -18,6 +18,7 @@ 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"; @@ -109,7 +110,9 @@ export const VisualContextProvider = ({ - {children} + + {children} + diff --git a/app/src/context/visualConfig/grid.tsx b/app/src/context/visualConfig/grid.tsx index c28e1e7a..e53b6dce 100644 --- a/app/src/context/visualConfig/grid.tsx +++ b/app/src/context/visualConfig/grid.tsx @@ -44,16 +44,16 @@ export const GridVisualConfigContextProvider = ({ { setNCols(initial?.nCols ?? 100); setNRows(initial?.nRows ?? 100); diff --git a/app/src/context/visualConfig/ribbons.tsx b/app/src/context/visualConfig/ribbons.tsx new file mode 100644 index 00000000..6b5d8dc1 --- /dev/null +++ b/app/src/context/visualConfig/ribbons.tsx @@ -0,0 +1,66 @@ +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/lib/appState.ts b/app/src/lib/appState.ts index 09d8d1b3..243ef5d2 100644 --- a/app/src/lib/appState.ts +++ b/app/src/lib/appState.ts @@ -45,7 +45,6 @@ const useAppState = create((set, _) => ({ noteCanvasInteraction: () => set((state) => { state.user.canvasInteractionEventTracker.addEvent(); - console.log("HERE"); return { user: { canvasInteractionEventTracker: