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;