fix custom gradients in audiomotion visualizer

This commit is contained in:
jeffvli
2025-12-28 17:45:21 -08:00
parent b26b6eab09
commit 3c07f03651
3 changed files with 125 additions and 153 deletions
@@ -1003,25 +1003,29 @@ const GeneralSettings = () => {
}; };
type CustomGradient = { type CustomGradient = {
colorStops: (string | { color: string; level?: number; pos?: number })[]; colorStops: StoredColorStop[];
dir?: string; dir?: string;
name: string; name: string;
}; };
type StoredColorStop = {
color: string;
level?: number;
levelEnabled?: boolean;
pos?: number;
positionEnabled?: boolean;
};
const CustomGradientsManager = () => { const CustomGradientsManager = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { updateProperty, visualizer } = useUpdateAudioMotionAnalyzer(); const { updateProperty, visualizer } = useUpdateAudioMotionAnalyzer();
const [isAdding, setIsAdding] = useState(false); const [isAdding, setIsAdding] = useState(false);
const [editingIndex, setEditingIndex] = useState<null | number>(null); const [editingIndex, setEditingIndex] = useState<null | number>(null);
const [newGradient, setNewGradient] = useState<CustomGradient>({ const [newGradient, setNewGradient] = useState<CustomGradient>({
colorStops: ['#ff0000'], colorStops: [{ color: '#ff0000', levelEnabled: false, positionEnabled: false }],
dir: 'v', dir: 'v',
name: '', name: '',
}); });
// Track which checkboxes are enabled for each color stop
const [colorStopOptions, setColorStopOptions] = useState<
Array<{ enableLevel: boolean; enablePos: boolean }>
>([{ enableLevel: false, enablePos: false }]);
const customGradients = visualizer.audiomotionanalyzer.customGradients || []; const customGradients = visualizer.audiomotionanalyzer.customGradients || [];
@@ -1030,8 +1034,19 @@ const CustomGradientsManager = () => {
const updatedGradients = [...customGradients, newGradient]; const updatedGradients = [...customGradients, newGradient];
updateProperty('customGradients', updatedGradients); updateProperty('customGradients', updatedGradients);
setNewGradient({ colorStops: ['#ff0000'], dir: 'v', name: '' }); setNewGradient({
setColorStopOptions([{ enableLevel: false, enablePos: false }]); colorStops: [
{
color: '#ff0000',
level: 0,
levelEnabled: false,
pos: 0,
positionEnabled: false,
},
],
dir: 'v',
name: '',
});
setIsAdding(false); setIsAdding(false);
}; };
@@ -1043,12 +1058,6 @@ const CustomGradientsManager = () => {
const handleEditGradient = (index: number) => { const handleEditGradient = (index: number) => {
const gradient = customGradients[index]; const gradient = customGradients[index];
setNewGradient(gradient); setNewGradient(gradient);
// Initialize checkbox states based on existing color stops
const options = gradient.colorStops.map((stop) => ({
enableLevel: typeof stop !== 'string' && stop.level !== undefined,
enablePos: typeof stop !== 'string' && stop.pos !== undefined,
}));
setColorStopOptions(options);
setEditingIndex(index); setEditingIndex(index);
setIsAdding(true); setIsAdding(true);
}; };
@@ -1059,15 +1068,21 @@ const CustomGradientsManager = () => {
const updatedGradients = [...customGradients]; const updatedGradients = [...customGradients];
updatedGradients[editingIndex] = newGradient; updatedGradients[editingIndex] = newGradient;
updateProperty('customGradients', updatedGradients); updateProperty('customGradients', updatedGradients);
setNewGradient({ colorStops: ['#ff0000'], dir: 'v', name: '' }); setNewGradient({
setColorStopOptions([{ enableLevel: false, enablePos: false }]); colorStops: [{ color: '#ff0000', levelEnabled: false, positionEnabled: false }],
dir: 'v',
name: '',
});
setEditingIndex(null); setEditingIndex(null);
setIsAdding(false); setIsAdding(false);
}; };
const handleCancel = () => { const handleCancel = () => {
setNewGradient({ colorStops: ['#ff0000'], dir: 'v', name: '' }); setNewGradient({
setColorStopOptions([{ enableLevel: false, enablePos: false }]); colorStops: [{ color: '#ff0000', levelEnabled: false, positionEnabled: false }],
dir: 'v',
name: '',
});
setEditingIndex(null); setEditingIndex(null);
setIsAdding(false); setIsAdding(false);
}; };
@@ -1075,9 +1090,11 @@ const CustomGradientsManager = () => {
const handleAddColorStop = () => { const handleAddColorStop = () => {
setNewGradient({ setNewGradient({
...newGradient, ...newGradient,
colorStops: [...newGradient.colorStops, '#00ff00'], colorStops: [
...newGradient.colorStops,
{ color: '#00ff00', levelEnabled: false, positionEnabled: false },
],
}); });
setColorStopOptions([...colorStopOptions, { enableLevel: false, enablePos: false }]);
}; };
const handleRemoveColorStop = (index: number) => { const handleRemoveColorStop = (index: number) => {
@@ -1086,33 +1103,16 @@ const CustomGradientsManager = () => {
...newGradient, ...newGradient,
colorStops: newGradient.colorStops.filter((_, i) => i !== index), colorStops: newGradient.colorStops.filter((_, i) => i !== index),
}); });
setColorStopOptions(colorStopOptions.filter((_, i) => i !== index));
}; };
const handleColorStopChange = (index: number, color: string) => { const handleColorStopChange = (index: number, color: string) => {
const updatedColorStops = [...newGradient.colorStops]; const updatedColorStops = [...newGradient.colorStops];
const currentStop = updatedColorStops[index]; const currentStop = updatedColorStops[index];
const options = colorStopOptions[index];
// If neither checkbox is enabled, store as string updatedColorStops[index] = {
if (!options.enablePos && !options.enableLevel) { ...currentStop,
updatedColorStops[index] = color; color,
} else { };
// Otherwise, store as object with enabled properties
updatedColorStops[index] = {
color,
...(options.enablePos &&
typeof currentStop !== 'string' &&
currentStop.pos !== undefined
? { pos: currentStop.pos }
: {}),
...(options.enableLevel &&
typeof currentStop !== 'string' &&
currentStop.level !== undefined
? { level: currentStop.level }
: {}),
};
}
setNewGradient({ ...newGradient, colorStops: updatedColorStops }); setNewGradient({ ...newGradient, colorStops: updatedColorStops });
}; };
@@ -1121,18 +1121,10 @@ const CustomGradientsManager = () => {
const updatedColorStops = [...newGradient.colorStops]; const updatedColorStops = [...newGradient.colorStops];
const currentStop = updatedColorStops[index]; const currentStop = updatedColorStops[index];
const posValue = typeof pos === 'number' ? pos : parseFloat(pos) || undefined; const posValue = typeof pos === 'number' ? pos : parseFloat(pos) || undefined;
const options = colorStopOptions[index];
const color = typeof currentStop === 'string' ? currentStop : currentStop.color;
updatedColorStops[index] = { updatedColorStops[index] = {
color, ...currentStop,
...(options.enablePos && posValue !== undefined ? { pos: posValue } : {}), ...(currentStop.positionEnabled && posValue !== undefined ? { pos: posValue } : {}),
...(options.enableLevel &&
typeof currentStop !== 'string' &&
currentStop.level !== undefined
? { level: currentStop.level }
: {}),
}; };
setNewGradient({ ...newGradient, colorStops: updatedColorStops }); setNewGradient({ ...newGradient, colorStops: updatedColorStops });
@@ -1142,87 +1134,43 @@ const CustomGradientsManager = () => {
const updatedColorStops = [...newGradient.colorStops]; const updatedColorStops = [...newGradient.colorStops];
const currentStop = updatedColorStops[index]; const currentStop = updatedColorStops[index];
const levelValue = typeof level === 'number' ? level : parseFloat(level) || undefined; const levelValue = typeof level === 'number' ? level : parseFloat(level) || undefined;
const options = colorStopOptions[index];
const color = typeof currentStop === 'string' ? currentStop : currentStop.color;
updatedColorStops[index] = { updatedColorStops[index] = {
color, ...currentStop,
...(options.enablePos && ...(currentStop.levelEnabled && levelValue !== undefined ? { level: levelValue } : {}),
typeof currentStop !== 'string' &&
currentStop.pos !== undefined
? { pos: currentStop.pos }
: {}),
...(options.enableLevel && levelValue !== undefined ? { level: levelValue } : {}),
}; };
setNewGradient({ ...newGradient, colorStops: updatedColorStops }); setNewGradient({ ...newGradient, colorStops: updatedColorStops });
}; };
const handleTogglePos = (index: number, enabled: boolean) => { const handleTogglePos = (index: number, enabled: boolean) => {
const updatedOptions = [...colorStopOptions]; const updatedColorStops = [...newGradient.colorStops];
updatedOptions[index] = { ...updatedOptions[index], enablePos: enabled }; const currentStop = updatedColorStops[index];
setColorStopOptions(updatedOptions);
// If both are now disabled, convert to string updatedColorStops[index] = {
if (!enabled && !updatedOptions[index].enableLevel) { ...currentStop,
const updatedColorStops = [...newGradient.colorStops]; positionEnabled: enabled,
const currentStop = updatedColorStops[index]; // Remove pos if disabling
const color = typeof currentStop === 'string' ? currentStop : currentStop.color; ...(enabled && currentStop.pos !== undefined ? { pos: currentStop.pos } : {}),
updatedColorStops[index] = color; ...(!enabled ? { pos: undefined } : {}),
setNewGradient({ ...newGradient, colorStops: updatedColorStops }); };
} else {
// Otherwise, ensure it's an object
const updatedColorStops = [...newGradient.colorStops];
const currentStop = updatedColorStops[index];
const color = typeof currentStop === 'string' ? currentStop : currentStop.color;
updatedColorStops[index] = { setNewGradient({ ...newGradient, colorStops: updatedColorStops });
color,
...(enabled && typeof currentStop !== 'string' && currentStop.pos !== undefined
? { pos: currentStop.pos }
: {}),
...(updatedOptions[index].enableLevel &&
typeof currentStop !== 'string' &&
currentStop.level !== undefined
? { level: currentStop.level }
: {}),
};
setNewGradient({ ...newGradient, colorStops: updatedColorStops });
}
}; };
const handleToggleLevel = (index: number, enabled: boolean) => { const handleToggleLevel = (index: number, enabled: boolean) => {
const updatedOptions = [...colorStopOptions]; const updatedColorStops = [...newGradient.colorStops];
updatedOptions[index] = { ...updatedOptions[index], enableLevel: enabled }; const currentStop = updatedColorStops[index];
setColorStopOptions(updatedOptions);
// If both are now disabled, convert to string updatedColorStops[index] = {
if (!enabled && !updatedOptions[index].enablePos) { ...currentStop,
const updatedColorStops = [...newGradient.colorStops]; levelEnabled: enabled,
const currentStop = updatedColorStops[index]; // Remove level if disabling
const color = typeof currentStop === 'string' ? currentStop : currentStop.color; ...(enabled && currentStop.level !== undefined ? { level: currentStop.level } : {}),
updatedColorStops[index] = color; ...(!enabled ? { level: undefined } : {}),
setNewGradient({ ...newGradient, colorStops: updatedColorStops }); };
} else {
// Otherwise, ensure it's an object
const updatedColorStops = [...newGradient.colorStops];
const currentStop = updatedColorStops[index];
const color = typeof currentStop === 'string' ? currentStop : currentStop.color;
updatedColorStops[index] = { setNewGradient({ ...newGradient, colorStops: updatedColorStops });
color,
...(updatedOptions[index].enablePos &&
typeof currentStop !== 'string' &&
currentStop.pos !== undefined
? { pos: currentStop.pos }
: {}),
...(enabled && typeof currentStop !== 'string' && currentStop.level !== undefined
? { level: currentStop.level }
: {}),
};
setNewGradient({ ...newGradient, colorStops: updatedColorStops });
}
}; };
return ( return (
@@ -1311,10 +1259,6 @@ const CustomGradientsManager = () => {
</Button> </Button>
</Group> </Group>
{newGradient.colorStops.map((stop, index) => { {newGradient.colorStops.map((stop, index) => {
const options = colorStopOptions[index] || {
enableLevel: false,
enablePos: false,
};
return ( return (
<Group grow key={index}> <Group grow key={index}>
<ColorInput <ColorInput
@@ -1323,20 +1267,18 @@ const CustomGradientsManager = () => {
handleColorStopChange(index, color) handleColorStopChange(index, color)
} }
size="sm" size="sm"
value={typeof stop === 'string' ? stop : stop.color} value={stop.color}
/> />
<VisualizerSlider <VisualizerSlider
defaultValue={ defaultValue={stop.pos}
typeof stop === 'string' ? undefined : stop.pos disabled={!stop.positionEnabled}
}
disabled={!options.enablePos}
label={ label={
<Group <Group
gap="xs" gap="xs"
style={{ alignItems: 'center' }} style={{ alignItems: 'center' }}
> >
<Checkbox <Checkbox
checked={options.enablePos} checked={stop.positionEnabled || false}
onChange={(e) => onChange={(e) =>
handleTogglePos( handleTogglePos(
index, index,
@@ -1358,19 +1300,15 @@ const CustomGradientsManager = () => {
step={0.1} step={0.1}
/> />
<VisualizerSlider <VisualizerSlider
defaultValue={ defaultValue={stop.level}
typeof stop === 'string' disabled={!stop.levelEnabled}
? undefined
: stop.level
}
disabled={!options.enableLevel}
label={ label={
<Group <Group
gap="xs" gap="xs"
style={{ alignItems: 'center' }} style={{ alignItems: 'center' }}
> >
<Checkbox <Checkbox
checked={options.enableLevel} checked={stop.levelEnabled || false}
onChange={(e) => onChange={(e) =>
handleToggleLevel( handleToggleLevel(
index, index,
@@ -117,6 +117,50 @@ const VisualizerInner = () => {
}; };
}, [visualizer, gradientsRegistered, isCustomGradient]); }, [visualizer, gradientsRegistered, isCustomGradient]);
const transformGradientForVisualizer = useCallback(
(gradient: {
colorStops: Array<{
color: string;
level?: number;
levelEnabled?: boolean;
pos?: number;
positionEnabled?: boolean;
}>;
dir?: string;
}): {
colorStops: (string | { color: string; level?: number; pos?: number })[];
dir?: string;
} => {
const transformedColorStops = gradient.colorStops.map((stop) => {
// If neither position nor level is enabled, return just the color string
if (!stop.positionEnabled && !stop.levelEnabled) {
return stop.color;
}
// Otherwise, return an object with only enabled properties
const transformedStop: { color: string; level?: number; pos?: number } = {
color: stop.color,
};
if (stop.positionEnabled && stop.pos !== undefined) {
transformedStop.pos = stop.pos;
}
if (stop.levelEnabled && stop.level !== undefined) {
transformedStop.level = stop.level;
}
return transformedStop;
});
return {
colorStops: transformedColorStops,
...(gradient.dir ? { dir: gradient.dir } : {}),
};
},
[],
);
const registerCustomGradients = useCallback( const registerCustomGradients = useCallback(
(audioMotionInstance: AudioMotionAnalyzer) => { (audioMotionInstance: AudioMotionAnalyzer) => {
if (visualizer.type !== 'audiomotionanalyzer') { if (visualizer.type !== 'audiomotionanalyzer') {
@@ -127,18 +171,8 @@ const VisualizerInner = () => {
customGradients.forEach((gradient) => { customGradients.forEach((gradient) => {
try { try {
const gradientConfig: { const gradientConfig = transformGradientForVisualizer(gradient);
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); audioMotionInstance.registerGradient(gradient.name, gradientConfig as any);
} catch (error) { } catch (error) {
console.error(`Failed to register gradient "${gradient.name}":`, error); console.error(`Failed to register gradient "${gradient.name}":`, error);
@@ -148,7 +182,7 @@ const VisualizerInner = () => {
// Mark gradients as registered // Mark gradients as registered
setGradientsRegistered(true); setGradientsRegistered(true);
}, },
[visualizer], [visualizer, transformGradientForVisualizer],
); );
useEffect(() => { useEffect(() => {
+7 -7
View File
@@ -243,13 +243,13 @@ const AudioMotionAnalyzerSettingsSchema = z.object({
customGradients: z.array( customGradients: z.array(
z.object({ z.object({
colorStops: z.array( colorStops: z.array(
z.string().or( z.object({
z.object({ color: z.string(),
color: z.string(), level: z.number().min(0).max(1).optional(),
level: z.number().min(0).max(1).optional(), levelEnabled: z.boolean().optional(),
pos: z.number().min(0).max(1).optional(), pos: z.number().min(0).max(1).optional(),
}), positionEnabled: z.boolean().optional(),
), }),
), ),
dir: z.string().optional(), dir: z.string().optional(),
name: z.string(), name: z.string(),