From 3c07f03651ab15497af35f2734ec9166f6fb731c Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sun, 28 Dec 2025 17:45:21 -0800 Subject: [PATCH] fix custom gradients in audiomotion visualizer --- .../visualizer-settings-form.tsx | 206 ++++++------------ .../audiomotionanalyzer/visualizer.tsx | 58 ++++- src/renderer/store/settings.store.ts | 14 +- 3 files changed, 125 insertions(+), 153 deletions(-) diff --git a/src/renderer/features/visualizer/components/audiomotionanalyzer/visualizer-settings-form.tsx b/src/renderer/features/visualizer/components/audiomotionanalyzer/visualizer-settings-form.tsx index 783726ff3..1f749e326 100644 --- a/src/renderer/features/visualizer/components/audiomotionanalyzer/visualizer-settings-form.tsx +++ b/src/renderer/features/visualizer/components/audiomotionanalyzer/visualizer-settings-form.tsx @@ -1003,25 +1003,29 @@ const GeneralSettings = () => { }; type CustomGradient = { - colorStops: (string | { color: string; level?: number; pos?: number })[]; + colorStops: StoredColorStop[]; dir?: string; name: string; }; +type StoredColorStop = { + color: string; + level?: number; + levelEnabled?: boolean; + pos?: number; + positionEnabled?: boolean; +}; + const CustomGradientsManager = () => { const { t } = useTranslation(); const { updateProperty, visualizer } = useUpdateAudioMotionAnalyzer(); const [isAdding, setIsAdding] = useState(false); const [editingIndex, setEditingIndex] = useState(null); const [newGradient, setNewGradient] = useState({ - colorStops: ['#ff0000'], + colorStops: [{ color: '#ff0000', levelEnabled: false, positionEnabled: false }], dir: 'v', 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 || []; @@ -1030,8 +1034,19 @@ const CustomGradientsManager = () => { const updatedGradients = [...customGradients, newGradient]; updateProperty('customGradients', updatedGradients); - setNewGradient({ colorStops: ['#ff0000'], dir: 'v', name: '' }); - setColorStopOptions([{ enableLevel: false, enablePos: false }]); + setNewGradient({ + colorStops: [ + { + color: '#ff0000', + level: 0, + levelEnabled: false, + pos: 0, + positionEnabled: false, + }, + ], + dir: 'v', + name: '', + }); setIsAdding(false); }; @@ -1043,12 +1058,6 @@ const CustomGradientsManager = () => { const handleEditGradient = (index: number) => { const gradient = customGradients[index]; 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); setIsAdding(true); }; @@ -1059,15 +1068,21 @@ const CustomGradientsManager = () => { const updatedGradients = [...customGradients]; updatedGradients[editingIndex] = newGradient; updateProperty('customGradients', updatedGradients); - setNewGradient({ colorStops: ['#ff0000'], dir: 'v', name: '' }); - setColorStopOptions([{ enableLevel: false, enablePos: false }]); + setNewGradient({ + colorStops: [{ color: '#ff0000', levelEnabled: false, positionEnabled: false }], + dir: 'v', + name: '', + }); setEditingIndex(null); setIsAdding(false); }; const handleCancel = () => { - setNewGradient({ colorStops: ['#ff0000'], dir: 'v', name: '' }); - setColorStopOptions([{ enableLevel: false, enablePos: false }]); + setNewGradient({ + colorStops: [{ color: '#ff0000', levelEnabled: false, positionEnabled: false }], + dir: 'v', + name: '', + }); setEditingIndex(null); setIsAdding(false); }; @@ -1075,9 +1090,11 @@ const CustomGradientsManager = () => { const handleAddColorStop = () => { setNewGradient({ ...newGradient, - colorStops: [...newGradient.colorStops, '#00ff00'], + colorStops: [ + ...newGradient.colorStops, + { color: '#00ff00', levelEnabled: false, positionEnabled: false }, + ], }); - setColorStopOptions([...colorStopOptions, { enableLevel: false, enablePos: false }]); }; const handleRemoveColorStop = (index: number) => { @@ -1086,33 +1103,16 @@ const CustomGradientsManager = () => { ...newGradient, colorStops: newGradient.colorStops.filter((_, i) => i !== index), }); - setColorStopOptions(colorStopOptions.filter((_, i) => i !== index)); }; const handleColorStopChange = (index: number, color: string) => { const updatedColorStops = [...newGradient.colorStops]; const currentStop = updatedColorStops[index]; - const options = colorStopOptions[index]; - // If neither checkbox is enabled, store as string - if (!options.enablePos && !options.enableLevel) { - updatedColorStops[index] = 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 } - : {}), - }; - } + updatedColorStops[index] = { + ...currentStop, + color, + }; setNewGradient({ ...newGradient, colorStops: updatedColorStops }); }; @@ -1121,18 +1121,10 @@ const CustomGradientsManager = () => { const updatedColorStops = [...newGradient.colorStops]; const currentStop = updatedColorStops[index]; const posValue = typeof pos === 'number' ? pos : parseFloat(pos) || undefined; - const options = colorStopOptions[index]; - - const color = typeof currentStop === 'string' ? currentStop : currentStop.color; updatedColorStops[index] = { - color, - ...(options.enablePos && posValue !== undefined ? { pos: posValue } : {}), - ...(options.enableLevel && - typeof currentStop !== 'string' && - currentStop.level !== undefined - ? { level: currentStop.level } - : {}), + ...currentStop, + ...(currentStop.positionEnabled && posValue !== undefined ? { pos: posValue } : {}), }; setNewGradient({ ...newGradient, colorStops: updatedColorStops }); @@ -1142,87 +1134,43 @@ const CustomGradientsManager = () => { const updatedColorStops = [...newGradient.colorStops]; const currentStop = updatedColorStops[index]; const levelValue = typeof level === 'number' ? level : parseFloat(level) || undefined; - const options = colorStopOptions[index]; - - const color = typeof currentStop === 'string' ? currentStop : currentStop.color; updatedColorStops[index] = { - color, - ...(options.enablePos && - typeof currentStop !== 'string' && - currentStop.pos !== undefined - ? { pos: currentStop.pos } - : {}), - ...(options.enableLevel && levelValue !== undefined ? { level: levelValue } : {}), + ...currentStop, + ...(currentStop.levelEnabled && levelValue !== undefined ? { level: levelValue } : {}), }; setNewGradient({ ...newGradient, colorStops: updatedColorStops }); }; const handleTogglePos = (index: number, enabled: boolean) => { - const updatedOptions = [...colorStopOptions]; - updatedOptions[index] = { ...updatedOptions[index], enablePos: enabled }; - setColorStopOptions(updatedOptions); + const updatedColorStops = [...newGradient.colorStops]; + const currentStop = updatedColorStops[index]; - // If both are now disabled, convert to string - if (!enabled && !updatedOptions[index].enableLevel) { - const updatedColorStops = [...newGradient.colorStops]; - const currentStop = updatedColorStops[index]; - const color = typeof currentStop === 'string' ? currentStop : currentStop.color; - updatedColorStops[index] = color; - 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] = { + ...currentStop, + positionEnabled: enabled, + // Remove pos if disabling + ...(enabled && currentStop.pos !== undefined ? { pos: currentStop.pos } : {}), + ...(!enabled ? { pos: undefined } : {}), + }; - updatedColorStops[index] = { - 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 }); - } + setNewGradient({ ...newGradient, colorStops: updatedColorStops }); }; const handleToggleLevel = (index: number, enabled: boolean) => { - const updatedOptions = [...colorStopOptions]; - updatedOptions[index] = { ...updatedOptions[index], enableLevel: enabled }; - setColorStopOptions(updatedOptions); + const updatedColorStops = [...newGradient.colorStops]; + const currentStop = updatedColorStops[index]; - // If both are now disabled, convert to string - if (!enabled && !updatedOptions[index].enablePos) { - const updatedColorStops = [...newGradient.colorStops]; - const currentStop = updatedColorStops[index]; - const color = typeof currentStop === 'string' ? currentStop : currentStop.color; - updatedColorStops[index] = color; - 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] = { + ...currentStop, + levelEnabled: enabled, + // Remove level if disabling + ...(enabled && currentStop.level !== undefined ? { level: currentStop.level } : {}), + ...(!enabled ? { level: undefined } : {}), + }; - updatedColorStops[index] = { - 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 }); - } + setNewGradient({ ...newGradient, colorStops: updatedColorStops }); }; return ( @@ -1311,10 +1259,6 @@ const CustomGradientsManager = () => { {newGradient.colorStops.map((stop, index) => { - const options = colorStopOptions[index] || { - enableLevel: false, - enablePos: false, - }; return ( { handleColorStopChange(index, color) } size="sm" - value={typeof stop === 'string' ? stop : stop.color} + value={stop.color} /> handleTogglePos( index, @@ -1358,19 +1300,15 @@ const CustomGradientsManager = () => { step={0.1} /> handleToggleLevel( index, diff --git a/src/renderer/features/visualizer/components/audiomotionanalyzer/visualizer.tsx b/src/renderer/features/visualizer/components/audiomotionanalyzer/visualizer.tsx index 4ac268611..860404e78 100644 --- a/src/renderer/features/visualizer/components/audiomotionanalyzer/visualizer.tsx +++ b/src/renderer/features/visualizer/components/audiomotionanalyzer/visualizer.tsx @@ -117,6 +117,50 @@ const VisualizerInner = () => { }; }, [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( (audioMotionInstance: AudioMotionAnalyzer) => { if (visualizer.type !== 'audiomotionanalyzer') { @@ -127,18 +171,8 @@ const VisualizerInner = () => { customGradients.forEach((gradient) => { try { - const gradientConfig: { - colorStops: (string | { color: string; level?: number; pos?: number })[]; - dir?: string; - } = { - colorStops: gradient.colorStops, - }; + const gradientConfig = transformGradientForVisualizer(gradient); - 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); @@ -148,7 +182,7 @@ const VisualizerInner = () => { // Mark gradients as registered setGradientsRegistered(true); }, - [visualizer], + [visualizer, transformGradientForVisualizer], ); useEffect(() => { diff --git a/src/renderer/store/settings.store.ts b/src/renderer/store/settings.store.ts index 733b178ce..2d617cd83 100644 --- a/src/renderer/store/settings.store.ts +++ b/src/renderer/store/settings.store.ts @@ -243,13 +243,13 @@ const AudioMotionAnalyzerSettingsSchema = z.object({ customGradients: z.array( z.object({ colorStops: z.array( - z.string().or( - z.object({ - color: z.string(), - level: z.number().min(0).max(1).optional(), - pos: z.number().min(0).max(1).optional(), - }), - ), + z.object({ + color: z.string(), + level: z.number().min(0).max(1).optional(), + levelEnabled: z.boolean().optional(), + pos: z.number().min(0).max(1).optional(), + positionEnabled: z.boolean().optional(), + }), ), dir: z.string().optional(), name: z.string(),