diff --git a/app/src/components/canvas/Visual3D.tsx b/app/src/components/canvas/Visual3D.tsx
index ca10665a..ff17263d 100644
--- a/app/src/components/canvas/Visual3D.tsx
+++ b/app/src/components/canvas/Visual3D.tsx
@@ -12,7 +12,7 @@ import { APPLICATION_MODE } from "@/lib/applicationModes";
import { OrbitControls } from "@react-three/drei";
import { Canvas, useFrame, useThree } from "@react-three/fiber";
-import { MaybePaletteTracker } from "./paletteTracker";
+import { PaletteTracker } from "./paletteTracker";
const VisualizerComponent = ({
mode,
@@ -94,7 +94,7 @@ const Visual3DCanvas = ({
{/* */}
-
+
);
};
diff --git a/app/src/components/canvas/common.tsx b/app/src/components/canvas/common.tsx
index b0780959..10d85ee9 100644
--- a/app/src/components/canvas/common.tsx
+++ b/app/src/components/canvas/common.tsx
@@ -1,8 +1,10 @@
import { useVisualContext } from "@/context/visual";
+import { usePalette } from "@/lib/appState";
import { ColorPalette } from "@/lib/palettes";
const useBackgroundColor = () => {
- const { palette, colorBackground } = useVisualContext();
+ const { colorBackground } = useVisualContext();
+ const palette = usePalette();
return colorBackground
? ColorPalette.getPalette(palette).calcBackgroundColor(0)
: "#010204";
diff --git a/app/src/components/canvas/paletteTracker.tsx b/app/src/components/canvas/paletteTracker.tsx
index 2517c16e..9d7af794 100644
--- a/app/src/components/canvas/paletteTracker.tsx
+++ b/app/src/components/canvas/paletteTracker.tsx
@@ -1,9 +1,8 @@
-import { useState } from "react";
-import { useVisualContext, useVisualContextSetters } from "@/context/visual";
-import { useEnergyInfo } from "@/lib/appState";
+import { useVisualContext } from "@/context/visual";
+import { ScalarMovingAvgEventDetector } from "@/lib/analyzers/eventDetector";
+import { useAppStateActions, useEnergyInfo } from "@/lib/appState";
import { type IScalarTracker } from "@/lib/mappers/valueTracker/common";
import { EnergyTracker } from "@/lib/mappers/valueTracker/energyTracker";
-import { AVAILABLE_COLOR_PALETTES } from "@/lib/palettes";
import { useFrame } from "@react-three/fiber";
const PaletteUpdater = ({
@@ -11,41 +10,26 @@ const PaletteUpdater = ({
}: {
scalarTracker: IScalarTracker;
}) => {
- const threshold = 0.5;
- const frameSpan = 10;
- const { setPalette } = useVisualContextSetters();
- const [movingAvg, setMovingAvg] = useState(0);
+ const { paletteTrackEnergy: enabled } = useVisualContext();
+ const detector = new ScalarMovingAvgEventDetector(0.5, 50, 500);
+ const { nextPalette } = useAppStateActions();
useFrame(() => {
- const curr = scalarTracker.getNormalizedValue();
- if (movingAvg < threshold && curr > threshold) {
- setPalette((prev) => {
- const currIdx = AVAILABLE_COLOR_PALETTES.indexOf(prev) ?? 0;
- const nextIdx = (currIdx + 1) % AVAILABLE_COLOR_PALETTES.length;
- return AVAILABLE_COLOR_PALETTES[nextIdx];
- });
- setMovingAvg(1);
- } else {
- setMovingAvg((prev) => {
- return (prev * (frameSpan - 1) + curr) / frameSpan;
- });
+ if (!enabled) {
+ return;
+ }
+
+ if (detector.step(scalarTracker.getNormalizedValue())) {
+ nextPalette();
}
});
return <>>;
};
-const PaletteTracker = () => {
+export const PaletteTracker = () => {
const energyInfo = useEnergyInfo();
const scalarTracker = new EnergyTracker(energyInfo);
return ;
};
-
-export const MaybePaletteTracker = () => {
- const { paletteTrackEnergy } = useVisualContext();
- if (!paletteTrackEnergy) {
- return null;
- }
- return ;
-};
diff --git a/app/src/components/controls/visualSettingsSheet.tsx b/app/src/components/controls/visualSettingsSheet.tsx
index 39e95bf3..7356ba6a 100644
--- a/app/src/components/controls/visualSettingsSheet.tsx
+++ b/app/src/components/controls/visualSettingsSheet.tsx
@@ -6,6 +6,7 @@ import { Switch } from "@/components/ui/switch";
import { useModeContext } from "@/context/mode";
import { useVisualContext, useVisualContextSetters } from "@/context/visual";
import { APPLICATION_MODE } from "@/lib/applicationModes";
+import { useAppStateActions, usePalette } from "@/lib/appState";
import {
AVAILABLE_COLOR_PALETTES,
ColorPalette,
@@ -67,6 +68,7 @@ const VisualSettingsControls = () => {
case "diffusedRing":
return DiffusedRingVisualSettingsControls();
case "dna":
+ case "boxes":
return null;
default:
return visual satisfies never;
@@ -76,9 +78,12 @@ const VisualSettingsControls = () => {
export const VisualSettingsSheet = ({ children }: PropsWithChildren) => {
const [open, setOpen] = useState(false);
const { mode } = useModeContext();
- const { colorBackground, palette, paletteTrackEnergy } = useVisualContext();
- const { setColorBackground, setPalette, setPaletteTrackEnergy } =
+ const { colorBackground, paletteTrackEnergy } = useVisualContext();
+ const { setColorBackground, setPaletteTrackEnergy } =
useVisualContextSetters();
+ const palette = usePalette();
+ const { setPalette } = useAppStateActions();
+
return (
{children}
diff --git a/app/src/components/controls/visualsDock.tsx b/app/src/components/controls/visualsDock.tsx
index 6b3d1759..eb57bfc4 100644
--- a/app/src/components/controls/visualsDock.tsx
+++ b/app/src/components/controls/visualsDock.tsx
@@ -4,7 +4,7 @@ import {
type VisualType,
} from "@/components/visualizers/common";
import { useVisualContext, useVisualContextSetters } from "@/context/visual";
-import { Box, CircleDashed, Dna, Globe, Grid3x3 } from "lucide-react";
+import { Box, Boxes, CircleDashed, Dna, Globe, Grid3x3 } from "lucide-react";
import { Dock, DockItem, DockNav } from "./dock";
@@ -20,6 +20,8 @@ const VisualIcon = ({ visual }: { visual: VisualType }) => {
return ;
case "dna":
return ;
+ case "boxes":
+ return ;
default:
return visual satisfies never;
}
diff --git a/app/src/components/visualizers/audioScope/reactive.tsx b/app/src/components/visualizers/audioScope/reactive.tsx
index 0190f749..131ca609 100644
--- a/app/src/components/visualizers/audioScope/reactive.tsx
+++ b/app/src/components/visualizers/audioScope/reactive.tsx
@@ -1,12 +1,14 @@
import { useEffect } from "react";
-import { useVisualContext, useVisualContextSetters } from "@/context/visual";
+import { useVisualContextSetters } from "@/context/visual";
+import { useAppStateActions, usePalette } from "@/lib/appState";
import { ColorPalette } from "@/lib/palettes";
import BaseScopeVisual, { type TextureMapper } from "./base";
const ScopeVisual = ({ textureMapper }: { textureMapper: TextureMapper }) => {
- const { palette } = useVisualContext();
- const { setColorBackground, setPalette } = useVisualContextSetters();
+ const palette = usePalette();
+ const { setPalette } = useAppStateActions();
+ const { setColorBackground } = useVisualContextSetters();
const color = ColorPalette.getPalette(palette).lerpColor(0.5);
const usePoints = true;
diff --git a/app/src/components/visualizers/boxes/base.tsx b/app/src/components/visualizers/boxes/base.tsx
new file mode 100644
index 00000000..366cb2ba
--- /dev/null
+++ b/app/src/components/visualizers/boxes/base.tsx
@@ -0,0 +1,118 @@
+import { useEffect, useMemo, useRef } from "react";
+import { ScalarMovingAvgEventDetector } from "@/lib/analyzers/eventDetector";
+import { usePalette } from "@/lib/appState";
+import { type IScalarTracker } from "@/lib/mappers/valueTracker/common";
+import { ColorPalette } from "@/lib/palettes";
+import { useFrame } from "@react-three/fiber";
+import {
+ BoxGeometry,
+ Matrix4,
+ MeshBasicMaterial,
+ type InstancedMesh,
+} from "three";
+
+const BaseBoxes = ({
+ scalarTracker,
+ nBoxes = 5,
+ gridSize = 10,
+ cellSize = 0.25,
+}: {
+ scalarTracker: IScalarTracker;
+ nBoxes?: number;
+ gridSize?: number;
+ cellSize?: number;
+}) => {
+ const rotateDurationMs = 250;
+ const nRows = gridSize;
+ const nCols = gridSize;
+ const detector = useMemo(
+ () => new ScalarMovingAvgEventDetector(0.65, 150, 2 * rotateDurationMs),
+ [rotateDurationMs],
+ );
+ const meshRef = useRef(null!);
+ const tmpMatrix = useMemo(() => new Matrix4(), []);
+ const palette = usePalette();
+ const lut = ColorPalette.getPalette(palette).buildLut();
+
+ const cellAssignments = useMemo(
+ () =>
+ Array.from({ length: nBoxes }, (_) => {
+ const row = Math.floor(nRows * Math.random());
+ const col = Math.floor(nCols * Math.random());
+ return {
+ fromRow: row,
+ fromCol: col,
+ toRow: row,
+ toCol: col,
+ };
+ }),
+ [nBoxes, nRows, nCols],
+ );
+
+ // Recolor;
+ useEffect(() => {
+ for (let instanceIdx = 0; instanceIdx < nBoxes; instanceIdx++) {
+ meshRef.current.setColorAt(
+ instanceIdx,
+ lut.getColor(instanceIdx / (nBoxes - 1)),
+ );
+ }
+ meshRef.current.instanceColor!.needsUpdate = true;
+ }, [lut, nBoxes]);
+
+ useFrame(() => {
+ if (detector.step(scalarTracker?.getNormalizedValue() ?? 0)) {
+ // random jitter
+ const rowJitter = Math.floor(Math.random() * 3) - 1;
+ const colJitter = Math.floor(Math.random() * 3) - 1;
+ for (let i = 0; i < nBoxes; i++) {
+ cellAssignments[i].fromRow = cellAssignments[i].toRow;
+ cellAssignments[i].fromCol = cellAssignments[i].toCol;
+ cellAssignments[i].toRow += (Math.random() > 0.5 ? 1 : -1) * colJitter;
+ cellAssignments[i].toCol += (Math.random() > 0.5 ? 1 : -1) * rowJitter;
+ }
+ }
+
+ const alpha = Math.min(
+ 1,
+ Math.max(0, detector.timeSinceLastEventMs / rotateDurationMs),
+ );
+
+ let normCubeX, normCubeY, x, y, z;
+ cellAssignments.forEach(
+ ({ fromRow, fromCol, toRow, toCol }, instanceIdx) => {
+ const row = fromRow + alpha * (toRow - fromRow);
+ const col = fromCol + alpha * (toCol - fromCol);
+
+ // Find a random cell
+ normCubeX = row / (nRows - 1);
+ normCubeY = col / (nCols - 1);
+
+ x = nRows * cellSize * (normCubeX - 0.5);
+ y = nCols * cellSize * (normCubeY - 0.5);
+ z = 0;
+ // Position
+ tmpMatrix.setPosition(x, y, z);
+
+ meshRef.current.setMatrixAt(instanceIdx, tmpMatrix);
+ },
+ );
+
+ // Update the instance
+ meshRef.current.instanceMatrix.needsUpdate = true;
+ });
+
+ return (
+
+
+
+
+ );
+};
+
+export default BaseBoxes;
diff --git a/app/src/components/visualizers/boxes/reactive.tsx b/app/src/components/visualizers/boxes/reactive.tsx
new file mode 100644
index 00000000..ccbcecb2
--- /dev/null
+++ b/app/src/components/visualizers/boxes/reactive.tsx
@@ -0,0 +1,29 @@
+import { type VisualProps } from "@/components/visualizers/common";
+import Ground from "@/components/visualizers/ground";
+import { Vector3 } from "three";
+
+import BaseBoxes from "./base";
+
+const BoxesVisual = ({ scalarTracker }: VisualProps) => {
+ const nBoxes = 100;
+ const gridSize = 100;
+ const cellSize = 0.25;
+
+ return (
+ <>
+ Math.sin(0.0025 * Date.now()) + 1,
+ }
+ }
+ nBoxes={nBoxes}
+ gridSize={gridSize}
+ cellSize={cellSize}
+ />
+
+ >
+ );
+};
+
+export default BoxesVisual;
diff --git a/app/src/components/visualizers/common.ts b/app/src/components/visualizers/common.ts
index 18e8f707..4a7b4660 100644
--- a/app/src/components/visualizers/common.ts
+++ b/app/src/components/visualizers/common.ts
@@ -18,6 +18,7 @@ export const AVAILABLE_VISUALS = [
"cube",
"diffusedRing",
"dna",
+ "boxes",
// "stencil",
// "swarm",
] as const;
diff --git a/app/src/components/visualizers/cube/base.tsx b/app/src/components/visualizers/cube/base.tsx
index 957a7c43..1c658807 100644
--- a/app/src/components/visualizers/cube/base.tsx
+++ b/app/src/components/visualizers/cube/base.tsx
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef } from "react";
-import { useVisualContext } from "@/context/visual";
+import { usePalette } from "@/lib/appState";
import {
COORDINATE_TYPE,
HALF_DIAGONAL_UNIT_SQUARE,
@@ -32,7 +32,7 @@ const BaseCube = ({
const inputCoordinateType = volume
? COORDINATE_TYPE.CARTESIAN_3D
: COORDINATE_TYPE.CARTESIAN_CUBE_FACES;
- const { palette } = useVisualContext();
+ const palette = usePalette();
const lut = ColorPalette.getPalette(palette).buildLut();
// Recolor
diff --git a/app/src/components/visualizers/dna/base.tsx b/app/src/components/visualizers/dna/base.tsx
index db12b972..190e09f1 100644
--- a/app/src/components/visualizers/dna/base.tsx
+++ b/app/src/components/visualizers/dna/base.tsx
@@ -1,5 +1,5 @@
import { forwardRef, useEffect, useMemo, useRef } from "react";
-import { useVisualContext } from "@/context/visual";
+import { usePalette } from "@/lib/appState";
import {
COORDINATE_TYPE,
TWO_PI,
@@ -91,7 +91,7 @@ const BaseDoubleHelix = forwardRef<
},
ref,
) => {
- const { palette } = useVisualContext();
+ const palette = usePalette();
const lut = ColorPalette.getPalette(palette).buildLut();
const nBasePairs = Math.floor(helixLength / baseSpacing);
const refBaseMesh = useRef(null!);
diff --git a/app/src/components/visualizers/grid/base.tsx b/app/src/components/visualizers/grid/base.tsx
index 5b9f065c..e8957234 100644
--- a/app/src/components/visualizers/grid/base.tsx
+++ b/app/src/components/visualizers/grid/base.tsx
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef } from "react";
-import { useVisualContext } from "@/context/visual";
+import { usePalette } from "@/lib/appState";
import {
COORDINATE_TYPE,
type ICoordinateMapper,
@@ -28,7 +28,7 @@ const BaseGrid = ({
}) => {
const meshRef = useRef(null!);
const tmpMatrix = useMemo(() => new Matrix4(), []);
- const { palette } = useVisualContext();
+ const palette = usePalette();
const lut = ColorPalette.getPalette(palette).buildLut();
// Recolor
diff --git a/app/src/components/visualizers/sphere/base.tsx b/app/src/components/visualizers/sphere/base.tsx
index c3c581bf..cff7ea47 100644
--- a/app/src/components/visualizers/sphere/base.tsx
+++ b/app/src/components/visualizers/sphere/base.tsx
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef } from "react";
-import { useVisualContext } from "@/context/visual";
+import { usePalette } from "@/lib/appState";
import {
COORDINATE_TYPE,
TWO_PI,
@@ -29,9 +29,9 @@ const BaseSphere = ({
nPoints?: number;
cubeSideLength?: number;
}) => {
+ const palette = usePalette();
const meshRef = useRef(null!);
const tmpMatrix = useMemo(() => new Matrix4(), []);
- const { palette } = useVisualContext();
const lut = ColorPalette.getPalette(palette).buildLut();
useEffect(() => {
for (let i = 0; i < nPoints; i++) {
diff --git a/app/src/components/visualizers/visualizerAudio.tsx b/app/src/components/visualizers/visualizerAudio.tsx
index 9b8c51cc..851d6200 100644
--- a/app/src/components/visualizers/visualizerAudio.tsx
+++ b/app/src/components/visualizers/visualizerAudio.tsx
@@ -13,6 +13,7 @@ const AudioVisual = ({ visual }: { visual: VisualType }) => {
const coordinateMapper = new CoordinateMapper_Data(amplitude, freqData);
const energyTracker = new EnergyTracker(energyInfo);
+
const VisualComponent = useMemo(
() =>
lazy(
diff --git a/app/src/context/visual.tsx b/app/src/context/visual.tsx
index 9c4e50ff..2600cdd1 100644
--- a/app/src/context/visual.tsx
+++ b/app/src/context/visual.tsx
@@ -12,16 +12,19 @@ import {
type VisualType,
} from "@/components/visualizers/common";
import { APPLICATION_MODE } from "@/lib/applicationModes";
-import { COLOR_PALETTE, type AVAILABLE_COLOR_PALETTES } from "@/lib/palettes";
import { useModeContext } from "./mode";
-import { CombinedVisualsConfigContextProvider } from "./visualConfig/combined";
+import { CubeVisualConfigContextProvider } from "./visualConfig/cube";
+import { RingVisualConfigContextProvider } from "./visualConfig/diffusedRing";
+import { DnaVisualConfigContextProvider } from "./visualConfig/dna";
+import { GridVisualConfigContextProvider } from "./visualConfig/grid";
+import { SphereVisualConfigContextProvider } from "./visualConfig/sphere";
+// import { StencilVisualConfigContextProvider } from "./visualConfig/stencil";
+// import { SwarmVisualConfigContextProvider } from "./visualConfig/swarm";
import { useWaveGeneratorContextSetters } from "./waveGenerator";
-type Palette = (typeof AVAILABLE_COLOR_PALETTES)[number];
-export interface VisualConfig {
+interface VisualConfig {
visual: VisualType;
- palette: Palette;
colorBackground: boolean;
paletteTrackEnergy: boolean;
}
@@ -29,9 +32,7 @@ export interface VisualConfig {
export const VisualContext = createContext<{
config: VisualConfig;
setters: {
- resetConfig: Dispatch;
setVisual: Dispatch>;
- setPalette: Dispatch>;
setColorBackground: Dispatch>;
setPaletteTrackEnergy: Dispatch>;
};
@@ -43,22 +44,16 @@ export const VisualContextProvider = ({
}: PropsWithChildren<{
initial?: Partial;
}>) => {
- // const { setTheme } = useTheme();
const { mode } = useModeContext();
- const [key, setKey] = useState(0); // used to reset the context
const [visual, setVisual] = useState(
initial?.visual ?? AVAILABLE_VISUALS[0],
);
- const [palette, setPalette] = useState(
- initial?.palette ?? COLOR_PALETTE.THREE_COOL_TO_WARM,
- );
const [colorBackground, setColorBackground] = useState(
initial?.colorBackground ?? true,
);
const [paletteTrackEnergy, setPaletteTrackEnergy] = useState(
initial?.paletteTrackEnergy ?? false,
);
-
const { setWaveformFrequenciesHz, setMaxAmplitude } =
useWaveGeneratorContextSetters();
@@ -78,6 +73,7 @@ export const VisualContextProvider = ({
}
}, [visual, mode, setWaveformFrequenciesHz, setMaxAmplitude]);
+ // Reset paletteTrackEnergy whenever the mode changes
useEffect(() => {
switch (mode) {
case APPLICATION_MODE.WAVE_FORM:
@@ -87,33 +83,38 @@ export const VisualContextProvider = ({
setPaletteTrackEnergy(false);
break;
case APPLICATION_MODE.AUDIO:
+ setPaletteTrackEnergy(true);
break;
default:
return mode satisfies never;
}
}, [mode, setPaletteTrackEnergy]);
-
return (
setKey((key) => key + 1),
- setVisual: setVisual,
- setPalette: setPalette,
- setColorBackground: setColorBackground,
- setPaletteTrackEnergy: setPaletteTrackEnergy,
+ setVisual,
+ setColorBackground,
+ setPaletteTrackEnergy,
},
}}
>
-
- {children}
-
+
+
+
+
+
+ {children}
+
+
+
+
+
);
};
diff --git a/app/src/context/visualConfig/combined.tsx b/app/src/context/visualConfig/combined.tsx
deleted file mode 100644
index 9df68efe..00000000
--- a/app/src/context/visualConfig/combined.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { type PropsWithChildren } from "react";
-
-import { CubeVisualConfigContextProvider } from "./cube";
-import { RingVisualConfigContextProvider } from "./diffusedRing";
-import { DnaVisualConfigContextProvider } from "./dna";
-import { GridVisualConfigContextProvider } from "./grid";
-import { SphereVisualConfigContextProvider } from "./sphere";
-import { StencilVisualConfigContextProvider } from "./stencil";
-import { SwarmVisualConfigContextProvider } from "./swarm";
-
-export const CombinedVisualsConfigContextProvider = ({
- children,
-}: PropsWithChildren) => {
- return (
-
-
-
-
-
-
-
- {children}
-
-
-
-
-
-
-
- );
-};
diff --git a/app/src/lib/analyzers/eventDetector.ts b/app/src/lib/analyzers/eventDetector.ts
new file mode 100644
index 00000000..5d01a710
--- /dev/null
+++ b/app/src/lib/analyzers/eventDetector.ts
@@ -0,0 +1,74 @@
+import { Clock } from "three";
+
+export interface IEventDetector {
+ timeSinceLastEventMs: number;
+ step(scalar: number): boolean;
+}
+
+export class ScalarMovingAvgEventDetector implements IEventDetector {
+ private clock = new Clock(true);
+ private bufferSize = 1000;
+ private lastEventElapsedMs = 0;
+ public get timeSinceLastEventMs() {
+ return this.clock.elapsedTime * 1000 - this.lastEventElapsedMs;
+ }
+ private buffer: {
+ value: number;
+ elapsedTimeMs: number;
+ }[] = Array.from({ length: this.bufferSize }).map((_) => ({
+ value: 0,
+ elapsedTimeMs: 0,
+ }));
+
+ private threshold: number;
+ private windowSizeMs: number;
+ private cooldownMs: number;
+ private observationCount = 0;
+
+ constructor(threshold = 0.5, windowSizeMs = 150, cooldownMs = 500) {
+ this.threshold = threshold;
+ this.windowSizeMs = windowSizeMs;
+ this.cooldownMs = cooldownMs;
+ }
+
+ private getBufferAvg(windowEndTimestampMs: number) {
+ const start = windowEndTimestampMs - this.windowSizeMs;
+ const end = windowEndTimestampMs;
+ const stats = this.buffer.reduce(
+ (acc, entry) => {
+ if (entry.elapsedTimeMs < start || entry.elapsedTimeMs > end) {
+ return acc;
+ }
+ return {
+ sum: acc.sum + entry.value,
+ count: acc.count + 1,
+ };
+ },
+ { sum: 0, count: 0 },
+ );
+ return stats.count > 0 ? stats.sum / stats.count : 0;
+ }
+
+ public step(scalar: number) {
+ const ms = this.clock.getElapsedTime() * 1000;
+ // Add the observation
+ const idx = this.observationCount % this.bufferSize;
+ this.buffer[idx].value = scalar;
+ this.buffer[idx].elapsedTimeMs = ms;
+ this.observationCount++;
+
+ // Can't trigger in cooldown
+ if (this.timeSinceLastEventMs < this.cooldownMs) {
+ return false;
+ }
+
+ // Check for trigger
+ const avg = this.getBufferAvg(ms);
+ if (avg > this.threshold) {
+ // reset
+ this.lastEventElapsedMs = ms;
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/app/src/lib/appState.ts b/app/src/lib/appState.ts
index da44f0f8..4061835e 100644
--- a/app/src/lib/appState.ts
+++ b/app/src/lib/appState.ts
@@ -1,6 +1,15 @@
import { create } from "zustand";
+import {
+ AVAILABLE_COLOR_PALETTES,
+ COLOR_PALETTE,
+ type ColorPaletteType,
+} from "./palettes";
+
interface IAppState {
+ visual: {
+ palette: ColorPaletteType;
+ };
visualSourceData: {
x: Float32Array;
y: Float32Array;
@@ -9,17 +18,37 @@ interface IAppState {
current: number;
};
actions: {
+ setPalette: (newPalette: ColorPaletteType) => void;
+ nextPalette: () => void;
resizeVisualSourceData: (newSize: number) => void;
};
}
const useAppState = create((set, _) => ({
+ visual: {
+ palette: COLOR_PALETTE.THREE_COOL_TO_WARM,
+ },
visualSourceData: {
x: new Float32Array(121).fill(0),
y: new Float32Array(121).fill(0),
},
energyInfo: { current: 0 },
actions: {
+ setPalette: (newPalette: ColorPaletteType) =>
+ set((_) => {
+ return {
+ visual: { palette: newPalette },
+ };
+ }),
+ nextPalette: () =>
+ set((state) => {
+ const currIdx =
+ AVAILABLE_COLOR_PALETTES.indexOf(state.visual.palette) ?? 0;
+ const nextIdx = (currIdx + 1) % AVAILABLE_COLOR_PALETTES.length;
+ return {
+ visual: { palette: AVAILABLE_COLOR_PALETTES[nextIdx] },
+ };
+ }),
resizeVisualSourceData: (newSize: number) =>
set((_) => {
return {
@@ -32,6 +61,7 @@ const useAppState = create((set, _) => ({
},
}));
+export const usePalette = () => useAppState((state) => state.visual.palette);
export const useVisualSourceDataX = () =>
useAppState((state) => state.visualSourceData.x);
export const useVisualSourceDataY = () =>
diff --git a/app/src/lib/palettes.ts b/app/src/lib/palettes.ts
index c3a95a7b..cb47a4cf 100644
--- a/app/src/lib/palettes.ts
+++ b/app/src/lib/palettes.ts
@@ -18,7 +18,6 @@ export const COLOR_PALETTE = {
NATURAL: "Natural",
NATURAL_2: "Natural_2",
CIRCUS: "Circus",
- CIRCUS_2: "Circus_2",
SEASIDE: "Seaside",
DRAGON: "Dragon",
} as const;
@@ -40,7 +39,6 @@ export const AVAILABLE_COLOR_PALETTES = [
COLOR_PALETTE.NATURAL,
COLOR_PALETTE.NATURAL_2,
COLOR_PALETTE.CIRCUS,
- COLOR_PALETTE.CIRCUS_2,
COLOR_PALETTE.SEASIDE,
COLOR_PALETTE.DRAGON,
];
@@ -241,15 +239,6 @@ export class ColorPalette implements IColorPalette {
"#544C98",
"#ECACBC",
]);
- case COLOR_PALETTE.CIRCUS_2:
- return new ColorPalette(COLOR_PALETTE.CIRCUS_2, [
- "#F62D62",
- "#FFFFFF",
- "#FDB600",
- "#F42D2D",
- "#544C98",
- "#ECACBC",
- ]);
case COLOR_PALETTE.SEASIDE:
return new ColorPalette(COLOR_PALETTE.SEASIDE, [
"#FEB019",
diff --git a/app/src/lib/soundcloud/api.ts b/app/src/lib/soundcloud/api.ts
index bc073a3e..5db763a3 100644
--- a/app/src/lib/soundcloud/api.ts
+++ b/app/src/lib/soundcloud/api.ts
@@ -50,7 +50,11 @@ export const getUserTracks = async ({
});
// Sort descending by playback count
- return tracks.sort((a, b) => b.playback_count - a.playback_count);
+ return tracks.sort(
+ (a, b) =>
+ (b.playback_count ?? Number.POSITIVE_INFINITY) -
+ (a.playback_count ?? Number.POSITIVE_INFINITY),
+ );
};
export const getTrackStreamUrl = async (id: number) => {
diff --git a/app/src/lib/soundcloud/models.ts b/app/src/lib/soundcloud/models.ts
index 244ea33b..5feb4fea 100644
--- a/app/src/lib/soundcloud/models.ts
+++ b/app/src/lib/soundcloud/models.ts
@@ -13,7 +13,7 @@ export const TrackSchema = z.object({
id: z.number(),
title: z.string(),
artwork_url: z.string().nullable(),
- playback_count: z.number(),
+ playback_count: z.number().nullable(),
user: UserSchema.optional(),
});
export type SoundcloudTrack = z.infer;