mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-08 04:50:12 +02:00
Add visualizer configuration (#1443)
* add visualizer configuration * add visualizer presets * add butterchurn visualizer * wrap visualizers in error boundary
This commit is contained in:
+11
@@ -0,0 +1,11 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--theme-spacing-md);
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.select-label {
|
||||
text-align: center;
|
||||
}
|
||||
+2040
File diff suppressed because it is too large
Load Diff
+5
@@ -0,0 +1,5 @@
|
||||
import { VisualizerSettingsForm } from './visualizer-settings-form';
|
||||
|
||||
export const VisualizerSettingsContextModal = () => {
|
||||
return <VisualizerSettingsForm />;
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
.container {
|
||||
position: relative;
|
||||
z-index: 50;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: auto;
|
||||
|
||||
canvas {
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.settings-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container .settings-icon {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.visualizer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
import AudioMotionAnalyzer from 'audiomotion-analyzer';
|
||||
import { createRef, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import styles from './visualizer.module.css';
|
||||
|
||||
import { useWebAudio } from '/@/renderer/features/player/hooks/use-webaudio';
|
||||
import { openVisualizerSettingsModal } from '/@/renderer/features/player/utils/open-visualizer-settings-modal';
|
||||
import { ComponentErrorBoundary } from '/@/renderer/features/shared/components/component-error-boundary';
|
||||
import { useSettingsStore } from '/@/renderer/store';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
|
||||
const VisualizerInner = () => {
|
||||
const { webAudio } = useWebAudio();
|
||||
const canvasRef = createRef<HTMLDivElement>();
|
||||
const accent = useSettingsStore((store) => store.general.accent);
|
||||
const visualizer = useSettingsStore((store) => store.visualizer);
|
||||
const [motion, setMotion] = useState<AudioMotionAnalyzer>();
|
||||
|
||||
// Check if a gradient name is a custom gradient
|
||||
const isCustomGradient = useCallback(
|
||||
(gradientName: string | undefined): boolean => {
|
||||
if (!gradientName || visualizer.type !== 'audiomotionanalyzer') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const customGradients = visualizer.audiomotionanalyzer.customGradients || [];
|
||||
return customGradients.some((gradient) => gradient.name === gradientName);
|
||||
},
|
||||
[visualizer],
|
||||
);
|
||||
|
||||
const [gradientsRegistered, setGradientsRegistered] = useState(false);
|
||||
|
||||
const options = useMemo(() => {
|
||||
if (visualizer.type !== 'audiomotionanalyzer') {
|
||||
return {};
|
||||
}
|
||||
|
||||
const ama = visualizer.audiomotionanalyzer;
|
||||
|
||||
const defaults = {
|
||||
bgAlpha: 0,
|
||||
showBgColor: false,
|
||||
};
|
||||
|
||||
const gradients: { gradient?: string; gradientLeft?: string; gradientRight?: string } = {};
|
||||
|
||||
// Use default gradient if custom gradient is selected but not yet registered
|
||||
const getSafeGradient = (gradientName: string | undefined): string => {
|
||||
if (!gradientName) return 'classic';
|
||||
if (isCustomGradient(gradientName)) {
|
||||
// Use default until custom gradients are registered
|
||||
return gradientsRegistered ? gradientName : 'classic';
|
||||
}
|
||||
return gradientName;
|
||||
};
|
||||
|
||||
if (ama.channelLayout === 'single') {
|
||||
gradients.gradient = getSafeGradient(ama.gradient);
|
||||
} else {
|
||||
gradients.gradientLeft = getSafeGradient(ama.gradientLeft);
|
||||
gradients.gradientRight = getSafeGradient(ama.gradientRight);
|
||||
}
|
||||
|
||||
return {
|
||||
...defaults,
|
||||
...gradients,
|
||||
alphaBars: ama.alphaBars,
|
||||
ansiBands: ama.ansiBands,
|
||||
barSpace: ama.barSpace,
|
||||
channelLayout: ama.channelLayout,
|
||||
colorMode: ama.colorMode,
|
||||
connectSpeakers: false,
|
||||
fadePeaks: ama.fadePeaks,
|
||||
fftSize: ama.fftSize,
|
||||
fillAlpha: ama.fillAlpha,
|
||||
frequencyScale: ama.frequencyScale,
|
||||
gravity: ama.gravity,
|
||||
ledBars: ama.ledBars,
|
||||
linearAmplitude: ama.linearAmplitude,
|
||||
linearBoost: ama.linearBoost,
|
||||
lineWidth: ama.lineWidth,
|
||||
loRes: ama.loRes,
|
||||
lumiBars: ama.lumiBars,
|
||||
maxDecibels: ama.maxDecibels,
|
||||
maxFPS: ama.maxFPS,
|
||||
maxFreq: ama.maxFreq,
|
||||
minDecibels: ama.minDecibels,
|
||||
minFreq: ama.minFreq,
|
||||
mirror: ama.mirror,
|
||||
mode: ama.mode,
|
||||
noteLabels: ama.noteLabels,
|
||||
outlineBars: ama.outlineBars,
|
||||
overlay: true,
|
||||
peakFadeTime: ama.peakFadeTime,
|
||||
peakHoldTime: ama.peakHoldTime,
|
||||
peakLine: ama.peakLine,
|
||||
radial: ama.radial,
|
||||
radialInvert: ama.radialInvert,
|
||||
radius: ama.radius,
|
||||
reflexAlpha: ama.reflexAlpha,
|
||||
reflexBright: ama.reflexBright,
|
||||
reflexFit: ama.reflexFit,
|
||||
reflexRatio: ama.reflexRatio,
|
||||
roundBars: ama.roundBars,
|
||||
showFPS: ama.showFPS,
|
||||
showPeaks: ama.showPeaks,
|
||||
showScaleX: ama.showScaleX,
|
||||
showScaleY: ama.showScaleY,
|
||||
smoothing: ama.smoothing,
|
||||
spinSpeed: ama.spinSpeed,
|
||||
splitGradient: ama.splitGradient,
|
||||
trueLeds: ama.trueLeds,
|
||||
volume: ama.volume,
|
||||
weightingFilter: (ama.weightingFilter || '') as any,
|
||||
};
|
||||
}, [visualizer, gradientsRegistered, isCustomGradient]);
|
||||
|
||||
const registerCustomGradients = useCallback(
|
||||
(audioMotionInstance: AudioMotionAnalyzer) => {
|
||||
if (visualizer.type !== 'audiomotionanalyzer') {
|
||||
return;
|
||||
}
|
||||
|
||||
const customGradients = visualizer.audiomotionanalyzer.customGradients || [];
|
||||
|
||||
customGradients.forEach((gradient) => {
|
||||
try {
|
||||
const gradientConfig: {
|
||||
colorStops: (string | { color: string; level?: number; pos?: number })[];
|
||||
dir?: string;
|
||||
} = {
|
||||
colorStops: gradient.colorStops,
|
||||
};
|
||||
|
||||
if (gradient.dir) {
|
||||
gradientConfig.dir = gradient.dir;
|
||||
}
|
||||
|
||||
// Type assertion needed as TypeScript definitions may be incomplete
|
||||
audioMotionInstance.registerGradient(gradient.name, gradientConfig as any);
|
||||
} catch (error) {
|
||||
console.error(`Failed to register gradient "${gradient.name}":`, error);
|
||||
}
|
||||
});
|
||||
|
||||
// Mark gradients as registered
|
||||
setGradientsRegistered(true);
|
||||
},
|
||||
[visualizer],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const { context, gains } = webAudio || {};
|
||||
if (gains && context && canvasRef.current && !motion) {
|
||||
// Reset gradients registered flag on new instance
|
||||
setGradientsRegistered(false);
|
||||
|
||||
// Create options without custom gradients on first init
|
||||
const initOptions: any = { ...options };
|
||||
|
||||
// Replace custom gradients with default 'classic' for initial setup
|
||||
if (visualizer.type === 'audiomotionanalyzer') {
|
||||
const ama = visualizer.audiomotionanalyzer;
|
||||
if (isCustomGradient(ama.gradient)) {
|
||||
initOptions.gradient = 'classic';
|
||||
}
|
||||
if (isCustomGradient(ama.gradientLeft)) {
|
||||
initOptions.gradientLeft = 'classic';
|
||||
}
|
||||
if (isCustomGradient(ama.gradientRight)) {
|
||||
initOptions.gradientRight = 'classic';
|
||||
}
|
||||
}
|
||||
|
||||
const audioMotion = new AudioMotionAnalyzer(canvasRef.current, {
|
||||
...initOptions,
|
||||
audioCtx: context,
|
||||
});
|
||||
|
||||
// Register custom gradients (this will set gradientsRegistered to true)
|
||||
registerCustomGradients(audioMotion);
|
||||
|
||||
setMotion(audioMotion);
|
||||
for (const gain of gains) audioMotion.connectInput(gain);
|
||||
}
|
||||
|
||||
return () => {};
|
||||
}, [
|
||||
accent,
|
||||
canvasRef,
|
||||
motion,
|
||||
registerCustomGradients,
|
||||
webAudio,
|
||||
visualizer,
|
||||
options,
|
||||
isCustomGradient,
|
||||
]);
|
||||
|
||||
// Re-register custom gradients when they change
|
||||
useEffect(() => {
|
||||
if (motion && visualizer.type === 'audiomotionanalyzer') {
|
||||
setGradientsRegistered(false);
|
||||
registerCustomGradients(motion);
|
||||
}
|
||||
}, [
|
||||
motion,
|
||||
registerCustomGradients,
|
||||
visualizer.audiomotionanalyzer.customGradients,
|
||||
visualizer.type,
|
||||
]);
|
||||
|
||||
// Update visualizer settings when they change
|
||||
useEffect(() => {
|
||||
if (motion) {
|
||||
motion.setOptions(options);
|
||||
}
|
||||
}, [motion, options]);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<ActionIcon
|
||||
className={styles.settingsIcon}
|
||||
icon="settings2"
|
||||
iconProps={{ size: 'lg' }}
|
||||
onClick={openVisualizerSettingsModal}
|
||||
pos="absolute"
|
||||
right={0}
|
||||
top={0}
|
||||
variant="transparent"
|
||||
/>
|
||||
<div className={styles.visualizer} ref={canvasRef} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Visualizer = () => {
|
||||
return (
|
||||
<ComponentErrorBoundary>
|
||||
<VisualizerInner />
|
||||
</ComponentErrorBoundary>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
declare module 'butterchurn' {
|
||||
export default butterchurn;
|
||||
}
|
||||
|
||||
declare module 'butterchurn-presets' {
|
||||
export default butterchurnPresets;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
.container {
|
||||
position: relative;
|
||||
z-index: 50;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: auto;
|
||||
|
||||
&:hover {
|
||||
.settings-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container .settings-icon {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.preset-overlay {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
padding: var(--theme-spacing-xs) var(--theme-spacing-sm);
|
||||
font-weight: 500;
|
||||
color: var(--theme-colors-foreground);
|
||||
pointer-events: none;
|
||||
background-color: rgb(0 0 0 / 50%);
|
||||
border-radius: 0 var(--theme-radius-md) 0 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.container:hover .preset-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
import butterchurn from 'butterchurn';
|
||||
import butterchurnPresets from 'butterchurn-presets';
|
||||
import { createRef, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import styles from './visualizer.module.css';
|
||||
|
||||
import { useWebAudio } from '/@/renderer/features/player/hooks/use-webaudio';
|
||||
import { openVisualizerSettingsModal } from '/@/renderer/features/player/utils/open-visualizer-settings-modal';
|
||||
import { ComponentErrorBoundary } from '/@/renderer/features/shared/components/component-error-boundary';
|
||||
import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store';
|
||||
import { usePlayerStatus } from '/@/renderer/store/player.store';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { PlayerStatus } from '/@/shared/types/types';
|
||||
|
||||
type ButterchurnVisualizer = {
|
||||
connectAudio: (audioNode: AudioNode) => void;
|
||||
loadPreset: (preset: any, blendTime: number) => void;
|
||||
render: () => void;
|
||||
setRendererSize: (width: number, height: number) => void;
|
||||
};
|
||||
|
||||
const VisualizerInner = () => {
|
||||
const { webAudio } = useWebAudio();
|
||||
const canvasRef = createRef<HTMLCanvasElement>();
|
||||
const containerRef = createRef<HTMLDivElement>();
|
||||
const [visualizer, setVisualizer] = useState<ButterchurnVisualizer>();
|
||||
const animationFrameRef = useRef<number | undefined>(undefined);
|
||||
const resizeObserverRef = useRef<ResizeObserver | undefined>(undefined);
|
||||
const cycleTimerRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
||||
const cycleStartTimeRef = useRef<number | undefined>(undefined);
|
||||
const butterchurnSettings = useSettingsStore((store) => store.visualizer.butterchurn);
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
const playerStatus = usePlayerStatus();
|
||||
const isPlaying = playerStatus === PlayerStatus.PLAYING;
|
||||
|
||||
useEffect(() => {
|
||||
const { context, gains } = webAudio || {};
|
||||
if (
|
||||
context &&
|
||||
gains &&
|
||||
canvasRef.current &&
|
||||
containerRef.current &&
|
||||
!visualizer &&
|
||||
isPlaying
|
||||
) {
|
||||
const canvas = canvasRef.current;
|
||||
const container = containerRef.current;
|
||||
|
||||
const getDimensions = () => {
|
||||
const rect = container.getBoundingClientRect();
|
||||
return {
|
||||
height: rect.height || 600,
|
||||
width: rect.width || 800,
|
||||
};
|
||||
};
|
||||
|
||||
let dimensions = getDimensions();
|
||||
|
||||
// If dimensions are 0, wait for next frame
|
||||
if (dimensions.width === 0 || dimensions.height === 0) {
|
||||
requestAnimationFrame(() => {
|
||||
dimensions = getDimensions();
|
||||
if (dimensions.width > 0 && dimensions.height > 0) {
|
||||
initializeVisualizer(dimensions.width, dimensions.height);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
initializeVisualizer(dimensions.width, dimensions.height);
|
||||
}
|
||||
|
||||
function initializeVisualizer(width: number, height: number) {
|
||||
if (!gains || gains.length === 0) return;
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
try {
|
||||
const butterchurnInstance = butterchurn.createVisualizer(context, canvas, {
|
||||
height,
|
||||
width,
|
||||
}) as ButterchurnVisualizer;
|
||||
|
||||
// Connect to audio gains (use the first gain node)
|
||||
butterchurnInstance.connectAudio(gains[0]);
|
||||
|
||||
// Load preset from settings or default
|
||||
const presets = butterchurnPresets.getPresets();
|
||||
const presetNames = Object.keys(presets);
|
||||
|
||||
if (presetNames.length > 0) {
|
||||
const presetName =
|
||||
butterchurnSettings.currentPreset &&
|
||||
presets[butterchurnSettings.currentPreset]
|
||||
? butterchurnSettings.currentPreset
|
||||
: presetNames[0];
|
||||
const preset = presets[presetName];
|
||||
butterchurnInstance.loadPreset(
|
||||
preset,
|
||||
butterchurnSettings.blendTime || 0.0,
|
||||
);
|
||||
// Initialize cycle timer
|
||||
cycleStartTimeRef.current = Date.now();
|
||||
}
|
||||
|
||||
setVisualizer(butterchurnInstance);
|
||||
} catch (error) {
|
||||
console.error('Failed to create butterchurn visualizer:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return () => {};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [webAudio, canvasRef, containerRef, visualizer, isPlaying]);
|
||||
|
||||
// Handle resize
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container || !visualizer) return;
|
||||
|
||||
const handleResize = () => {
|
||||
const rect = container.getBoundingClientRect();
|
||||
const width = rect.width;
|
||||
const height = rect.height;
|
||||
|
||||
if (canvasRef.current) {
|
||||
canvasRef.current.width = width;
|
||||
canvasRef.current.height = height;
|
||||
}
|
||||
|
||||
visualizer.setRendererSize(width, height);
|
||||
};
|
||||
|
||||
resizeObserverRef.current = new ResizeObserver(handleResize);
|
||||
resizeObserverRef.current.observe(container);
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, [visualizer, containerRef, canvasRef]);
|
||||
|
||||
// Update preset when currentPreset or blendTime changes (but not when cycling)
|
||||
const isCyclingRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visualizer || !butterchurnSettings.currentPreset) return;
|
||||
|
||||
// Skip if we're currently cycling (to avoid recreating the visualizer)
|
||||
if (isCyclingRef.current) {
|
||||
isCyclingRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const presets = butterchurnPresets.getPresets();
|
||||
const preset = presets[butterchurnSettings.currentPreset];
|
||||
|
||||
if (preset) {
|
||||
visualizer.loadPreset(preset, butterchurnSettings.blendTime || 0.0);
|
||||
// Reset cycle timer when preset changes manually
|
||||
cycleStartTimeRef.current = Date.now();
|
||||
}
|
||||
}, [visualizer, butterchurnSettings.currentPreset, butterchurnSettings.blendTime]);
|
||||
|
||||
// Handle preset cycling
|
||||
useEffect(() => {
|
||||
if (!visualizer || !butterchurnSettings.cyclePresets) {
|
||||
// Clear cycle timer if cycling is disabled
|
||||
if (cycleTimerRef.current) {
|
||||
clearInterval(cycleTimerRef.current);
|
||||
cycleTimerRef.current = undefined;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const presets = butterchurnPresets.getPresets();
|
||||
const allPresetNames = Object.keys(presets);
|
||||
|
||||
// Get the list of presets to cycle through
|
||||
const presetList = butterchurnSettings.includeAllPresets
|
||||
? allPresetNames
|
||||
: butterchurnSettings.selectedPresets.length > 0
|
||||
? butterchurnSettings.selectedPresets.filter((name) => presets[name])
|
||||
: allPresetNames;
|
||||
|
||||
if (presetList.length === 0) return;
|
||||
|
||||
// Reset cycle timer when settings change
|
||||
cycleStartTimeRef.current = Date.now();
|
||||
|
||||
const cycleToNextPreset = () => {
|
||||
if (!visualizer) return;
|
||||
|
||||
const currentPresetName = butterchurnSettings.currentPreset;
|
||||
let nextPresetName: string;
|
||||
|
||||
if (butterchurnSettings.randomizeNextPreset) {
|
||||
// Randomly select a preset (excluding current if there are multiple)
|
||||
const availablePresets =
|
||||
presetList.length > 1
|
||||
? presetList.filter((name) => name !== currentPresetName)
|
||||
: presetList;
|
||||
const randomIndex = Math.floor(Math.random() * availablePresets.length);
|
||||
nextPresetName = availablePresets[randomIndex];
|
||||
} else {
|
||||
// Cycle to next preset in order
|
||||
const currentIndex = currentPresetName ? presetList.indexOf(currentPresetName) : -1;
|
||||
const nextIndex =
|
||||
currentIndex >= 0 && currentIndex < presetList.length - 1
|
||||
? currentIndex + 1
|
||||
: 0;
|
||||
nextPresetName = presetList[nextIndex];
|
||||
}
|
||||
|
||||
const nextPreset = presets[nextPresetName];
|
||||
if (nextPreset) {
|
||||
// Get current settings to ensure we use the latest blendTime
|
||||
const currentSettings = useSettingsStore.getState().visualizer.butterchurn;
|
||||
|
||||
// Mark that we're cycling to prevent the preset change effect from running
|
||||
isCyclingRef.current = true;
|
||||
|
||||
// Load the preset with blending
|
||||
visualizer.loadPreset(nextPreset, currentSettings.blendTime || 0.0);
|
||||
|
||||
// Update currentPreset in settings
|
||||
const currentVisualizer = useSettingsStore.getState().visualizer;
|
||||
setSettings({
|
||||
visualizer: {
|
||||
...currentVisualizer,
|
||||
butterchurn: {
|
||||
...currentVisualizer.butterchurn,
|
||||
currentPreset: nextPresetName,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
cycleStartTimeRef.current = Date.now();
|
||||
}
|
||||
};
|
||||
|
||||
// Check every second if it's time to cycle
|
||||
cycleTimerRef.current = setInterval(() => {
|
||||
if (cycleStartTimeRef.current === undefined) {
|
||||
cycleStartTimeRef.current = Date.now();
|
||||
return;
|
||||
}
|
||||
const elapsed = (Date.now() - cycleStartTimeRef.current) / 1000; // Convert to seconds
|
||||
if (elapsed >= butterchurnSettings.cycleTime) {
|
||||
cycleToNextPreset();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
if (cycleTimerRef.current) {
|
||||
clearInterval(cycleTimerRef.current);
|
||||
cycleTimerRef.current = undefined;
|
||||
}
|
||||
};
|
||||
}, [visualizer, butterchurnSettings, setSettings]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visualizer) return;
|
||||
|
||||
let lastFrameTime = 0;
|
||||
const maxFPS = butterchurnSettings.maxFPS;
|
||||
const minFrameInterval = maxFPS > 0 ? 1000 / maxFPS : 0;
|
||||
|
||||
const render = (currentTime: number) => {
|
||||
if (maxFPS === 0 || currentTime - lastFrameTime >= minFrameInterval) {
|
||||
visualizer.render();
|
||||
lastFrameTime = currentTime;
|
||||
}
|
||||
animationFrameRef.current = requestAnimationFrame(render);
|
||||
};
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(render);
|
||||
|
||||
return () => {
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
}
|
||||
};
|
||||
}, [visualizer, butterchurnSettings.maxFPS]);
|
||||
|
||||
return (
|
||||
<div className={styles.container} ref={containerRef}>
|
||||
<ActionIcon
|
||||
className={styles.settingsIcon}
|
||||
icon="settings2"
|
||||
iconProps={{ size: 'lg' }}
|
||||
onClick={openVisualizerSettingsModal}
|
||||
pos="absolute"
|
||||
right={0}
|
||||
top={0}
|
||||
variant="transparent"
|
||||
/>
|
||||
<canvas className={styles.canvas} ref={canvasRef} />
|
||||
{butterchurnSettings.currentPreset && (
|
||||
<Text className={styles['preset-overlay']} isNoSelect size="sm">
|
||||
{butterchurnSettings.currentPreset}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Visualizer = () => {
|
||||
return (
|
||||
<ComponentErrorBoundary>
|
||||
<VisualizerInner />
|
||||
</ComponentErrorBoundary>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user