diff --git a/.github/workflows/pr_staging_deploy.yml b/.github/workflows/pr_staging_deploy.yml
index fa2a32c6..88934304 100644
--- a/.github/workflows/pr_staging_deploy.yml
+++ b/.github/workflows/pr_staging_deploy.yml
@@ -31,7 +31,7 @@ jobs:
git config --global user.name github-actions
git config --global user.email github-actions@github.com
- name: Checkout Repo
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
with:
path: "pr-build"
- uses: pnpm/action-setup@v2
@@ -45,7 +45,7 @@ jobs:
env:
CI: ""
- name: Checkout temporary deployment target repo
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
with:
repository: dcyoung/${{ env.PR_REPO_NAME }}
fetch-depth: 0
diff --git a/.github/workflows/pr_staging_teardown.yml b/.github/workflows/pr_staging_teardown.yml
index a68db189..30a71639 100644
--- a/.github/workflows/pr_staging_teardown.yml
+++ b/.github/workflows/pr_staging_teardown.yml
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Delete repository for temporary deployment
uses: dcyoung/ga-delete-git-repo@v1.0.0
with:
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 44bffe27..d007c414 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: 8
diff --git a/app/package.json b/app/package.json
index 7975bb92..854720d8 100644
--- a/app/package.json
+++ b/app/package.json
@@ -39,6 +39,7 @@
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.6",
"three": "^0.161.0",
+ "three-stdlib": "^2.29.4",
"zod": "^3.22.4",
"zod-fetch": "^0.1.1",
"zustand": "^4.5.0"
diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml
index 4ba66b0c..0a99d966 100644
--- a/app/pnpm-lock.yaml
+++ b/app/pnpm-lock.yaml
@@ -80,6 +80,9 @@ dependencies:
three:
specifier: ^0.161.0
version: 0.161.0
+ three-stdlib:
+ specifier: ^2.29.4
+ version: 2.29.4(three@0.161.0)
zod:
specifier: ^3.22.4
version: 3.22.4
diff --git a/app/src/components/canvas/AutoOrbitCamera.tsx b/app/src/components/canvas/AutoOrbitCamera.tsx
index de967c55..e5764327 100644
--- a/app/src/components/canvas/AutoOrbitCamera.tsx
+++ b/app/src/components/canvas/AutoOrbitCamera.tsx
@@ -2,6 +2,8 @@ import { useVisualContext } from "@/context/visual";
import { useFrame, useThree } from "@react-three/fiber";
import { Spherical, type Vector3 } from "three";
+import { VISUAL } from "../visualizers/common";
+
const setFromSphericalZUp = (vec: Vector3, s: Spherical) => {
const sinPhiRadius = Math.sin(s.phi) * s.radius;
vec.x = sinPhiRadius * Math.sin(s.theta);
@@ -16,7 +18,7 @@ const useSphericalLimits = () => {
// theta is the equator angle
// phi is the polar angle
switch (visual) {
- case "ribbons":
+ case VISUAL.RIBBONS:
return {
rMin: 10,
rMax: 15,
@@ -28,7 +30,7 @@ const useSphericalLimits = () => {
phiMax: Math.PI / 2.1,
phiSpeed: 0.25,
};
- case "sphere":
+ case VISUAL.SPHERE:
return {
rMin: 10,
rMax: 15,
@@ -40,7 +42,7 @@ const useSphericalLimits = () => {
phiMax: Math.PI / 2,
phiSpeed: 0.25,
};
- case "cube":
+ case VISUAL.CUBE:
return {
rMin: 12,
rMax: 20,
@@ -52,7 +54,7 @@ const useSphericalLimits = () => {
phiMax: Math.PI / 2,
phiSpeed: 0.25,
};
- case "diffusedRing":
+ case VISUAL.DIFFUSED_RING:
return {
rMin: 10,
rMax: 18,
@@ -64,9 +66,21 @@ const useSphericalLimits = () => {
phiMax: Math.PI / 2.25,
phiSpeed: 0.25,
};
- case "boxes":
- case "dna":
- case "grid":
+ case VISUAL.WALK:
+ return {
+ rMin: 15,
+ rMax: 22,
+ rSpeed: 0.1,
+ thetaMin: 0,
+ thetaMax: 2 * Math.PI,
+ thetaSpeed: 0.025,
+ phiMin: Math.PI / 3.5,
+ phiMax: Math.PI / 2.25,
+ phiSpeed: 0.25,
+ };
+ case VISUAL.BOXES:
+ case VISUAL.DNA:
+ case VISUAL.GRID:
return {
rMin: 15,
rMax: 22,
diff --git a/app/src/components/controls/dock.tsx b/app/src/components/controls/dock.tsx
index e9ebf9c4..ebdf6fd0 100644
--- a/app/src/components/controls/dock.tsx
+++ b/app/src/components/controls/dock.tsx
@@ -51,7 +51,7 @@ export const Dock = ({
return (
{
{showUI && (
- {mode !== APPLICATION_MODE.AUDIO_SCOPE &&
}
+ {mode !== APPLICATION_MODE.AUDIO_SCOPE && (
+
+ )}
{
1}
defaultChecked={waveformFrequenciesHz.length > 1}
onCheckedChange={(e) => {
setWaveformFrequenciesHz(e ? [2.0, 10.0] : [2.0]);
diff --git a/app/src/components/controls/visualSettingsSheet.tsx b/app/src/components/controls/visualSettingsSheet.tsx
index 10769081..1306bcf8 100644
--- a/app/src/components/controls/visualSettingsSheet.tsx
+++ b/app/src/components/controls/visualSettingsSheet.tsx
@@ -19,6 +19,7 @@ import {
} from "@/lib/palettes";
import { cn } from "@/lib/utils";
+import { VISUAL } from "../visualizers/common";
import { CubeVisualSettingsControls } from "./visual/cube";
import { DiffusedRingVisualSettingsControls } from "./visual/diffusedRing";
import { GridVisualSettingsControls } from "./visual/grid";
@@ -64,17 +65,18 @@ const PaletteIcon = ({
const VisualSettingsControls = () => {
const { visual } = useVisualContext();
switch (visual) {
- case "cube":
+ case VISUAL.CUBE:
return CubeVisualSettingsControls();
- case "grid":
+ case VISUAL.GRID:
return GridVisualSettingsControls();
- case "sphere":
+ case VISUAL.SPHERE:
return SphereVisualSettingsControls();
- case "diffusedRing":
+ case VISUAL.DIFFUSED_RING:
return DiffusedRingVisualSettingsControls();
- case "ribbons":
- case "dna":
- case "boxes":
+ case VISUAL.RIBBONS:
+ case VISUAL.DNA:
+ case VISUAL.BOXES:
+ case VISUAL.WALK:
return null;
default:
return visual satisfies never;
diff --git a/app/src/components/controls/visualsDock.tsx b/app/src/components/controls/visualsDock.tsx
index 435199df..71bc3234 100644
--- a/app/src/components/controls/visualsDock.tsx
+++ b/app/src/components/controls/visualsDock.tsx
@@ -1,6 +1,7 @@
import { type HTMLAttributes } from "react";
import {
AVAILABLE_VISUALS,
+ VISUAL,
type VisualType,
} from "@/components/visualizers/common";
import { useVisualContext, useVisualContextSetters } from "@/context/visual";
@@ -9,8 +10,10 @@ import {
Boxes,
CircleDashed,
Dna,
+ Footprints,
Globe,
Grid3x3,
+ HelpCircle,
Ribbon,
} from "lucide-react";
@@ -18,22 +21,24 @@ import { Dock, DockItem, DockNav } from "./dock";
const VisualIcon = ({ visual }: { visual: VisualType }) => {
switch (visual) {
- case "grid":
+ case VISUAL.GRID:
return ;
- case "cube":
+ case VISUAL.CUBE:
return ;
- case "sphere":
+ case VISUAL.SPHERE:
return ;
- case "diffusedRing":
+ case VISUAL.DIFFUSED_RING:
return ;
- case "dna":
+ case VISUAL.DNA:
return ;
- case "boxes":
+ case VISUAL.BOXES:
return ;
- case "ribbons":
+ case VISUAL.RIBBONS:
return ;
+ case VISUAL.WALK:
+ return ;
default:
- return visual satisfies never;
+ return ;
}
};
diff --git a/app/src/components/visualizers/common.ts b/app/src/components/visualizers/common.ts
index afaff8d6..26c30348 100644
--- a/app/src/components/visualizers/common.ts
+++ b/app/src/components/visualizers/common.ts
@@ -12,16 +12,18 @@ export interface MotionVisualProps {
scalarTracker?: IScalarTracker;
}
-export const AVAILABLE_VISUALS = [
- "grid",
- "sphere",
- "cube",
- "diffusedRing",
- "dna",
- "boxes",
- "ribbons",
- // "stencil",
- // "swarm",
-] as const;
+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 type VisualType = (typeof AVAILABLE_VISUALS)[number];
+export const AVAILABLE_VISUALS = Object.values(VISUAL);
+export type VisualType = (typeof VISUAL)[keyof typeof VISUAL];
diff --git a/app/src/components/visualizers/visual.tsx b/app/src/components/visualizers/visual.tsx
new file mode 100644
index 00000000..628974e7
--- /dev/null
+++ b/app/src/components/visualizers/visual.tsx
@@ -0,0 +1,26 @@
+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
index 851d6200..35950ad9 100644
--- a/app/src/components/visualizers/visualizerAudio.tsx
+++ b/app/src/components/visualizers/visualizerAudio.tsx
@@ -1,10 +1,11 @@
-import { lazy, Suspense, useMemo } from "react";
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();
@@ -14,22 +15,12 @@ const AudioVisual = ({ visual }: { visual: VisualType }) => {
const coordinateMapper = new CoordinateMapper_Data(amplitude, freqData);
const energyTracker = new EnergyTracker(energyInfo);
- const VisualComponent = useMemo(
- () =>
- lazy(
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
- async () => await import(`./${visual}/reactive.tsx`),
- ),
- [visual],
- );
-
return (
-
-
-
+
);
};
diff --git a/app/src/components/visualizers/visualizerNoise.tsx b/app/src/components/visualizers/visualizerNoise.tsx
index 62791033..ace0b68c 100644
--- a/app/src/components/visualizers/visualizerNoise.tsx
+++ b/app/src/components/visualizers/visualizerNoise.tsx
@@ -1,18 +1,10 @@
-import { lazy, Suspense, useMemo } from "react";
import { type VisualType } from "@/components/visualizers/common";
import { useNoiseGeneratorContext } from "@/context/noiseGenerator";
import { CoordinateMapper_Noise } from "@/lib/mappers/coordinateMappers/noise";
-const NoiseVisual = ({ visual }: { visual: VisualType }) => {
- const VisualComponent = useMemo(
- () =>
- lazy(
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
- async () => await import(`./${visual}/reactive.tsx`),
- ),
- [visual],
- );
+import { Visual } from "./visual";
+const NoiseVisual = ({ visual }: { visual: VisualType }) => {
const { amplitude, spatialScale, timeScale, nIterations } =
useNoiseGeneratorContext();
@@ -23,13 +15,7 @@ const NoiseVisual = ({ visual }: { visual: VisualType }) => {
nIterations,
);
- return (
- <>
-
-
-
- >
- );
+ return ;
};
export default NoiseVisual;
diff --git a/app/src/components/visualizers/visualizerWaveform.tsx b/app/src/components/visualizers/visualizerWaveform.tsx
index bfbf8e53..4b333d30 100644
--- a/app/src/components/visualizers/visualizerWaveform.tsx
+++ b/app/src/components/visualizers/visualizerWaveform.tsx
@@ -1,18 +1,10 @@
-import { lazy, Suspense, useMemo } from "react";
import { type VisualType } from "@/components/visualizers/common";
import { useWaveGeneratorContext } from "@/context/waveGenerator";
import { CoordinateMapper_WaveformSuperposition } from "@/lib/mappers/coordinateMappers/waveform";
-const WaveformVisual = ({ visual }: { visual: VisualType }) => {
- const VisualComponent = useMemo(
- () =>
- lazy(
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return
- async () => await import(`./${visual}/reactive.tsx`),
- ),
- [visual],
- );
+import { Visual } from "./visual";
+const WaveformVisual = ({ visual }: { visual: VisualType }) => {
const { maxAmplitude, waveformFrequenciesHz, amplitudeSplitRatio } =
useWaveGeneratorContext();
@@ -22,11 +14,7 @@ const WaveformVisual = ({ visual }: { visual: VisualType }) => {
amplitudeSplitRatio,
);
- return (
-
-
-
- );
+ return ;
};
export default WaveformVisual;
diff --git a/app/src/components/visualizers/walk/horse.png b/app/src/components/visualizers/walk/horse.png
new file mode 100644
index 00000000..66fb3026
Binary files /dev/null and b/app/src/components/visualizers/walk/horse.png differ
diff --git a/app/src/components/visualizers/walk/horse.tsx b/app/src/components/visualizers/walk/horse.tsx
new file mode 100644
index 00000000..29a1006c
--- /dev/null
+++ b/app/src/components/visualizers/walk/horse.tsx
@@ -0,0 +1,89 @@
+import { useEffect, useMemo, useRef } from "react";
+import { type VisualProps } from "@/components/visualizers/common";
+import { usePalette } from "@/lib/appState";
+import { ColorPalette } from "@/lib/palettes";
+import { useAnimations, useGLTF } from "@react-three/drei";
+import { useFrame } from "@react-three/fiber";
+import { type Group } from "three";
+import { type GLTF } from "three-stdlib";
+
+import MODEL_HORSE from "./horse.png";
+
+type GLTFResult = GLTF & {
+ nodes: {
+ mesh_0: THREE.Mesh;
+ };
+ materials: Record;
+ animations: GLTFAction[];
+};
+
+type ActionName = "horse_A_";
+
+interface GLTFAction extends THREE.AnimationClip {
+ name: ActionName;
+}
+
+const Horse = (_: VisualProps) => {
+ const group = useRef(null);
+ const { nodes, animations } = useGLTF(MODEL_HORSE) as GLTFResult;
+ const palette = usePalette();
+ const lut = ColorPalette.getPalette(palette).buildLut();
+
+ const material = useMemo(() => {
+ // const mat = nodes.mesh_0.material as MeshStandardMaterial;
+ return (
+
+ );
+ }, [lut]);
+
+ const { actions } = useAnimations(animations, group);
+
+ useEffect(() => {
+ actions?.horse_A_?.play();
+ });
+
+ useFrame(({ clock }) => {
+ const t = clock.getElapsedTime();
+
+ const rateOfChange = 0.5;
+ const tScale = (Math.sin(rateOfChange * t) + 1) / 2;
+ actions?.horse_A_?.setEffectiveTimeScale(tScale);
+ });
+
+ return (
+
+
+
+
+ {material}
+
+
+
+ );
+};
+
+export default Horse;
diff --git a/app/src/components/visualizers/walk/reactive.tsx b/app/src/components/visualizers/walk/reactive.tsx
new file mode 100644
index 00000000..fcea4ec9
--- /dev/null
+++ b/app/src/components/visualizers/walk/reactive.tsx
@@ -0,0 +1,15 @@
+import { type VisualProps } from "@/components/visualizers/common";
+
+import Horse from "./horse";
+import { Treadmill } from "./treadmill";
+
+const WalkVisual = ({ ...props }: VisualProps) => {
+ return (
+ <>
+
+
+ >
+ );
+};
+
+export default WalkVisual;
diff --git a/app/src/components/visualizers/walk/treadmill.tsx b/app/src/components/visualizers/walk/treadmill.tsx
new file mode 100644
index 00000000..73c59cef
--- /dev/null
+++ b/app/src/components/visualizers/walk/treadmill.tsx
@@ -0,0 +1,132 @@
+import { useEffect, useMemo, useRef } from "react";
+import { usePalette } from "@/lib/appState";
+import { easeInOut, EASING_FUNCTION } from "@/lib/easing";
+import { COORDINATE_TYPE } from "@/lib/mappers/coordinateMappers/common";
+import { ColorPalette } from "@/lib/palettes";
+import { useFrame } from "@react-three/fiber";
+import {
+ BoxGeometry,
+ CatmullRomCurve3,
+ Matrix4,
+ MeshStandardMaterial,
+ Quaternion,
+ Vector3,
+ type InstancedMesh,
+} from "three";
+
+import { type VisualProps } from "../common";
+
+const curvePoints = [
+ [-1, 1],
+ [-0.75, -0.1],
+ [-0.6, 0],
+ [0, 0],
+ [0.6, 0],
+ [0.75, -0.1],
+ [1, 1],
+] as const;
+
+export const Treadmill = ({
+ nStones = 30,
+ stoneWidth = 5,
+ stoneHeight = 0.1,
+ stoneLength = 1,
+ coordinateMapper,
+}: VisualProps & {
+ nStones?: number;
+ stoneWidth?: number;
+ stoneHeight?: number;
+ stoneLength?: number;
+}) => {
+ const stoneRef = useRef(null);
+ const [tmpMatrix, tmpVecPosition, tmpQuat, tmpVecScale] = useMemo(
+ () => [new Matrix4(), new Vector3(), new Quaternion(), new Vector3()],
+ [],
+ );
+
+ const curve = useMemo(() => {
+ const scale = 10.0;
+ return new CatmullRomCurve3(
+ curvePoints.map((v) => new Vector3(0, v[0], v[1]).multiplyScalar(scale)),
+ false,
+ "catmullrom",
+ 0.1,
+ );
+ }, []);
+
+ const palette = usePalette();
+ const lut = ColorPalette.getPalette(palette).buildLut();
+ useEffect(() => {
+ if (!stoneRef.current) {
+ return;
+ }
+ for (let instanceIdx = 0; instanceIdx < nStones; instanceIdx++) {
+ stoneRef.current.setColorAt(
+ instanceIdx,
+ lut.getColor(instanceIdx / (nStones - 1)),
+ );
+ }
+ stoneRef.current.instanceColor!.needsUpdate = true;
+ }, [stoneRef, lut, nStones]);
+
+ useFrame(({ clock }) => {
+ if (!stoneRef.current) {
+ return;
+ }
+ const t = clock.getElapsedTime();
+ const speed = 0.025;
+
+ const q = 0.5;
+ // assume v(t) = (sin(q * t) + 1) / 2... integrate to find position
+ const normPosition = (speed * (t - Math.cos(q * t) / q)) % 1;
+
+ const alphaRaw = normPosition;
+ const alpha = 1 - easeInOut(alphaRaw, EASING_FUNCTION.LINEAR);
+
+ for (let instanceIdx = 0; instanceIdx < nStones; instanceIdx++) {
+ const stoneAlpha = (alpha + instanceIdx / nStones) % 1;
+ const widthScalar = 1 - 2 * Math.abs(stoneAlpha - 0.5);
+ // const widthScalar = 1;
+
+ const dataAlpha = Math.abs(stoneAlpha - 0.5);
+ const mappedWidthScalar =
+ 0.5 +
+ coordinateMapper.map(COORDINATE_TYPE.CARTESIAN_1D, dataAlpha, t) / 2;
+ const finalWidthScalar = widthScalar * (1 + mappedWidthScalar);
+
+ //
+ stoneRef.current.getMatrixAt(instanceIdx, tmpMatrix);
+ tmpMatrix.decompose(tmpVecPosition, tmpQuat, tmpVecScale);
+
+ curve.getPointAt(stoneAlpha, tmpVecPosition);
+ tmpVecScale.set(finalWidthScalar, 1, 1);
+
+ tmpMatrix.compose(tmpVecPosition, tmpQuat, tmpVecScale);
+ stoneRef.current.setMatrixAt(instanceIdx, tmpMatrix);
+ }
+
+ stoneRef.current.instanceMatrix.needsUpdate = true;
+ });
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+};
diff --git a/app/src/context/visual.tsx b/app/src/context/visual.tsx
index 483e51cc..08ce7ce6 100644
--- a/app/src/context/visual.tsx
+++ b/app/src/context/visual.tsx
@@ -9,6 +9,7 @@ import {
} from "react";
import {
AVAILABLE_VISUALS,
+ VISUAL,
type VisualType,
} from "@/components/visualizers/common";
import { APPLICATION_MODE } from "@/lib/applicationModes";
@@ -62,7 +63,7 @@ export const VisualContextProvider = ({
useEffect(() => {
if (mode === APPLICATION_MODE.WAVE_FORM) {
switch (visual) {
- case "diffusedRing":
+ case VISUAL.DIFFUSED_RING:
setWaveformFrequenciesHz([2.0, 10.0]);
setMaxAmplitude(1.0);
break;
diff --git a/app/src/hooks/useVisualComponent.ts b/app/src/hooks/useVisualComponent.ts
new file mode 100644
index 00000000..cf44c16a
--- /dev/null
+++ b/app/src/hooks/useVisualComponent.ts
@@ -0,0 +1,14 @@
+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/vite.config.ts b/app/vite.config.ts
index 5e84c4c6..858ab509 100644
--- a/app/vite.config.ts
+++ b/app/vite.config.ts
@@ -11,4 +11,5 @@ export default defineConfig({
},
},
base: "/r3f-audio-visualizer/",
+ assetsInclude: ["**/*.glb"],
});