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: