Skip to content

Instantly share code, notes, and snippets.

@JacobFV
Last active November 8, 2024 06:01
Show Gist options
  • Save JacobFV/f3bb5bb665e6a984b5a8d251dcbf4d49 to your computer and use it in GitHub Desktop.
Save JacobFV/f3bb5bb665e6a984b5a8d251dcbf4d49 to your computer and use it in GitHub Desktop.
1D Traveling Wave Fourier Simulation

1D Traveling Wave Fourier Simulation

This visualization was completed for my thesis on measuring the dissapation of transverse sound waves in aqueous media under adiabatic conditions. Jk, its just a cool UV playground

Try it out here!

import React, { useState, useCallback, useEffect, useRef } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Switch } from '@/components/ui/switch';
import { Slider } from '@/components/ui/slider';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { LineChart, Line, XAxis, YAxis, CartesianGrid } from 'recharts';
const TRANSITION_DURATION = 3000;
const FPS_UPDATE_INTERVAL = 500;
const LED_COUNT = 20;
const DEFAULT_TIME_SPEED = 2;
// Updated presets to include RGB waves
const PRESETS = {
calm: {
name: "Calm",
channels: ['r', 'g', 'b'].map(() =>
Array(11).fill(null).map((_, i) => ({
amplitude: i === 0 ? 0.8 : i === 1 ? 0.3 : 0,
phase: 0
}))
),
description: "Gentle, in-phase waves"
},
excited: {
name: "Excited",
channels: ['r', 'g', 'b'].map((_, channelIndex) =>
Array(11).fill(null).map((_, i) => ({
amplitude: Math.exp(-i/3) * 0.8,
phase: i * Math.PI / 4 + (channelIndex * 2 * Math.PI / 3) // 120° phase shift between channels
}))
),
description: "Phase-shifted energy"
},
angry: {
name: "Angry",
channels: ['r', 'g', 'b'].map((_, channelIndex) =>
Array(11).fill(null).map((_, i) => ({
amplitude: i < 7 ? 0.7 - i * 0.05 : 0,
phase: (i % 2) * Math.PI + (channelIndex * Math.PI / 2)
}))
),
description: "Dissonant phases"
},
peaceful: {
name: "Peaceful",
channels: ['r', 'g', 'b'].map((_, channelIndex) =>
Array(11).fill(null).map((_, i) => ({
amplitude: 1 / (i + 1),
phase: i * Math.PI / 12 + (channelIndex * Math.PI / 4)
}))
),
description: "Harmonic phase alignment"
}
};
export default function WaveSimulation() {
const [channels, setChannels] = useState(PRESETS.calm.channels);
const [targetChannels, setTargetChannels] = useState(PRESETS.calm.channels);
const [isRunning, setIsRunning] = useState(true);
const [showIndividual, setShowIndividual] = useState(false);
const [currentPreset, setCurrentPreset] = useState('calm');
const [isTransitioning, setIsTransitioning] = useState(false);
const [showLEDs, setShowLEDs] = useState(false);
const [timeSpeed, setTimeSpeed] = useState(DEFAULT_TIME_SPEED);
const [fps, setFps] = useState(0);
const timeRef = useRef(0);
const lastFrameTimeRef = useRef(0);
const frameCountRef = useRef(0);
const lastFpsUpdateRef = useRef(0);
const animationFrameRef = useRef(null);
const chartDataRef = useRef([]);
// Calculate wave value for each channel
const calculateWaveValue = useCallback((x, time, channelIndex) => {
let sum = 0;
for (let n = 0; n <= 10; n++) {
const { amplitude, phase } = channels[channelIndex][n];
sum += amplitude * Math.sin(n * x - n * time + phase);
}
return sum;
}, [channels]);
const updateFPS = useCallback((currentTime) => {
frameCountRef.current++;
if (currentTime - lastFpsUpdateRef.current >= FPS_UPDATE_INTERVAL) {
setFps(Math.round((frameCountRef.current * 1000) / (currentTime - lastFpsUpdateRef.current)));
frameCountRef.current = 0;
lastFpsUpdateRef.current = currentTime;
}
}, []);
// Main animation loop
const animate = useCallback((currentTime) => {
updateFPS(currentTime);
if (!lastFrameTimeRef.current) lastFrameTimeRef.current = currentTime;
const deltaTime = (currentTime - lastFrameTimeRef.current) / 1000;
lastFrameTimeRef.current = currentTime;
// Update time only if animation is running
if (isRunning) {
timeRef.current = (timeRef.current + deltaTime * timeSpeed) % (2 * Math.PI);
}
if (showLEDs) {
const data = [];
for (let i = 0; i < LED_COUNT; i++) {
const x = (i / (LED_COUNT - 1)) * 2 * Math.PI;
const values = channels.map((_, channelIndex) =>
calculateWaveValue(x, timeRef.current, channelIndex)
);
data.push({
x,
values,
r: Math.max(0, Math.min(1, (values[0] + 2) / 4)), // Clamp between 0 and 1
g: Math.max(0, Math.min(1, (values[1] + 2) / 4)),
b: Math.max(0, Math.min(1, (values[2] + 2) / 4))
});
}
chartDataRef.current = data;
} else {
const points = 100;
const data = [];
for (let x = 0; x < points; x++) {
const xPos = (x / points) * 2 * Math.PI;
const values = channels.map((_, channelIndex) =>
calculateWaveValue(xPos, timeRef.current, channelIndex)
);
data.push({
x: xPos,
r: values[0],
g: values[1],
b: values[2]
});
}
chartDataRef.current = data;
}
animationFrameRef.current = requestAnimationFrame(animate);
}, [timeSpeed, channels, showLEDs, calculateWaveValue, updateFPS, isRunning]);
// Mode transition animation
useEffect(() => {
if (!isTransitioning) return;
const startTime = Date.now();
const startChannels = channels.map(channel => [...channel]);
const animateTransition = () => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / TRANSITION_DURATION, 1);
const newChannels = startChannels.map((channel, channelIndex) =>
channel.map((start, modeIndex) => {
const target = targetChannels[channelIndex][modeIndex];
return {
amplitude: start.amplitude + (target.amplitude - start.amplitude) * progress,
phase: start.phase + (target.phase - start.phase) * progress
};
})
);
setChannels(newChannels);
if (progress < 1) {
requestAnimationFrame(animateTransition);
} else {
setIsTransitioning(false);
}
};
requestAnimationFrame(animateTransition);
}, [isTransitioning, targetChannels]);
// Animation frame management - now runs continuously
useEffect(() => {
animationFrameRef.current = requestAnimationFrame(animate);
return () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, [animate]);
const applyPreset = useCallback((presetName) => {
setCurrentPreset(presetName);
setTargetChannels(PRESETS[presetName].channels);
setIsTransitioning(true);
}, []);
const LEDDisplay = useCallback(({ data }) => (
<div className="flex justify-between items-center h-64 w-full bg-gray-900 p-4 rounded-lg">
{data.map((point, i) => (
<div
key={i}
className="w-4 h-4 rounded-full transition-all duration-100"
style={{
backgroundColor: `rgb(${point.r * 255}, ${point.g * 255}, ${point.b * 255})`,
boxShadow: `0 0 10px 5px rgba(${point.r * 255}, ${point.g * 255}, ${point.b * 255}, 0.5)`
}}
/>
))}
</div>
), []);
return (
<Card className="w-full max-w-4xl">
<CardHeader>
<CardTitle className="flex justify-between items-center">
<span>RGB Wave Synthesis</span>
<span className="text-sm font-mono bg-gray-100 px-2 py-1 rounded">
{fps} FPS
</span>
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-6">
<div className="flex flex-wrap gap-4">
{Object.entries(PRESETS).map(([key, preset]) => (
<div key={key} className="flex flex-col items-center">
<Button
onClick={() => applyPreset(key)}
variant={currentPreset === key ? "default" : "outline"}
className="w-24"
>
{preset.name}
</Button>
<span className="text-xs text-gray-500 mt-1">{preset.description}</span>
</div>
))}
</div>
{showLEDs ? (
<LEDDisplay data={chartDataRef.current} />
) : (
<LineChart width={700} height={300} data={chartDataRef.current}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="x" />
<YAxis />
<Line
type="monotone"
dataKey="r"
stroke="#ef4444"
dot={false}
strokeWidth={2}
isAnimationActive={false}
/>
<Line
type="monotone"
dataKey="g"
stroke="#22c55e"
dot={false}
strokeWidth={2}
isAnimationActive={false}
/>
<Line
type="monotone"
dataKey="b"
stroke="#3b82f6"
dot={false}
strokeWidth={2}
isAnimationActive={false}
/>
</LineChart>
)}
<div className="space-y-4">
<div className="flex items-center space-x-4">
<Switch
checked={isRunning}
onCheckedChange={setIsRunning}
/>
<Label>Animate</Label>
<Switch
checked={showLEDs}
onCheckedChange={setShowLEDs}
/>
<Label>LED VU Meter</Label>
</div>
<div className="flex items-center space-x-4">
<Label className="w-24">Speed:</Label>
<Slider
value={[timeSpeed]}
min={0.1}
max={5}
step={0.1}
className="w-48"
onValueChange={([value]) => setTimeSpeed(value)}
/>
<span className="w-12 text-sm">{timeSpeed.toFixed(1)}x</span>
</div>
{['Red', 'Green', 'Blue'].map((channelName, channelIndex) => (
<div key={channelName} className="space-y-2">
<h3 className="font-bold">{channelName} Channel</h3>
<div className="grid grid-cols-2 gap-4">
{channels[channelIndex].map((mode, i) => (
<div key={i} className="space-y-2">
<div className="flex items-center space-x-4">
<Label className="w-24">Mode {i} Amp:</Label>
<Slider
value={[mode.amplitude]}
min={0}
max={1}
step={0.01}
className="w-48"
onValueChange={([value]) => {
const newChannels = channels.map((ch, idx) =>
idx === channelIndex
? ch.map((m, mi) =>
mi === i ? {...m, amplitude: value} : m
)
: ch
);
setChannels(newChannels);
setTargetChannels(newChannels);
}}
/>
</div>
<div className="flex items-center space-x-4">
<Label className="w-24">Mode {i} Phase:</Label>
<Slider
value={[mode.phase]}
min={0}
max={2 * Math.PI}
step={0.1}
className="w-48"
onValueChange={([value]) => {
const newChannels = channels.map((ch, idx) =>
idx === channelIndex
? ch.map((m, mi) =>
mi === i ? {...m, phase: value} : m
)
: ch
);
setChannels(newChannels);
setTargetChannels(newChannels);
}}
/>
</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
</CardContent>
</Card>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment