import { useState, useEffect, useCallback, useRef } from 'react'; import { getColors } from 'react-native-image-colors'; import type { ImageColorsResult } from 'react-native-image-colors'; interface DominantColorResult { dominantColor: string | null; loading: boolean; error: string | null; } // Simple in-memory cache for extracted colors const colorCache = new Map(); // Helper function to calculate color vibrancy const calculateVibrancy = (hex: string): number => { const r = parseInt(hex.substr(1, 2), 16); const g = parseInt(hex.substr(3, 2), 16); const b = parseInt(hex.substr(5, 2), 16); const max = Math.max(r, g, b); const min = Math.min(r, g, b); const saturation = max === 0 ? 0 : (max - min) / max; return saturation * (max / 255); }; // Helper function to calculate color brightness const calculateBrightness = (hex: string): number => { const r = parseInt(hex.substr(1, 2), 16); const g = parseInt(hex.substr(3, 2), 16); const b = parseInt(hex.substr(5, 2), 16); return (r * 299 + g * 587 + b * 114) / 1000; }; // Helper function to darken a color const darkenColor = (hex: string, factor: number = 0.1): string => { const r = parseInt(hex.substr(1, 2), 16); const g = parseInt(hex.substr(3, 2), 16); const b = parseInt(hex.substr(5, 2), 16); const newR = Math.floor(r * factor); const newG = Math.floor(g * factor); const newB = Math.floor(b * factor); return `#${newR.toString(16).padStart(2, '0')}${newG.toString(16).padStart(2, '0')}${newB.toString(16).padStart(2, '0')}`; }; // Enhanced color selection logic const selectBestColor = (result: ImageColorsResult): string => { let candidates: string[] = []; if (result.platform === 'android') { // Collect all available colors candidates = [ result.dominant, result.vibrant, result.darkVibrant, result.muted, result.darkMuted, result.lightVibrant, result.lightMuted, result.average ].filter(Boolean); } else if (result.platform === 'ios') { candidates = [ result.primary, result.secondary, result.background, result.detail ].filter(Boolean); } else if (result.platform === 'web') { candidates = [ result.dominant, result.vibrant, result.darkVibrant, result.muted, result.darkMuted, result.lightVibrant, result.lightMuted ].filter(Boolean); } if (candidates.length === 0) { return '#1a1a1a'; } // Score each color based on vibrancy and appropriateness for backgrounds const scoredColors = candidates.map(color => { const brightness = calculateBrightness(color); const vibrancy = calculateVibrancy(color); // Prefer colors that are: // 1. Not too bright (good for backgrounds) // 2. Have decent vibrancy (not too gray) // 3. Not too dark (still visible) let score = 0; // Brightness scoring (prefer medium-dark colors) if (brightness >= 30 && brightness <= 120) { score += 3; } else if (brightness >= 15 && brightness <= 150) { score += 2; } else if (brightness >= 5) { score += 1; } // Vibrancy scoring (prefer some color over pure gray) if (vibrancy >= 0.3) { score += 3; } else if (vibrancy >= 0.15) { score += 2; } else if (vibrancy >= 0.05) { score += 1; } return { color, score, brightness, vibrancy }; }); // Sort by score (highest first) scoredColors.sort((a, b) => b.score - a.score); // Get the best color let bestColor = scoredColors[0].color; const bestBrightness = scoredColors[0].brightness; // Apply more aggressive darkening to make colors darker overall if (bestBrightness > 60) { bestColor = darkenColor(bestColor, 0.18); } else if (bestBrightness > 40) { bestColor = darkenColor(bestColor, 0.3); } else if (bestBrightness > 20) { bestColor = darkenColor(bestColor, 0.5); } else { bestColor = darkenColor(bestColor, 0.7); } return bestColor; }; // Preload function to start extraction early export const preloadDominantColor = async (imageUri: string | null) => { if (!imageUri || colorCache.has(imageUri)) return; if (__DEV__) console.log('[useDominantColor] Preloading color for URI:', imageUri); try { // Use highest quality for best color accuracy const result = await getColors(imageUri, { fallback: '#1a1a1a', cache: true, key: imageUri, quality: 'highest', // Best quality for accurate colors pixelSpacing: 1, // Sample every pixel for best accuracy (Android only) }); const extractedColor = selectBestColor(result); colorCache.set(imageUri, extractedColor); } catch (err) { if (__DEV__) console.warn('[preloadDominantColor] Failed to preload color:', err); colorCache.set(imageUri, '#1a1a1a'); } }; export const useDominantColor = (imageUri: string | null): DominantColorResult => { // Start with cached color if available, otherwise use fallback immediately const [dominantColor, setDominantColor] = useState(() => { if (imageUri && colorCache.has(imageUri)) { return colorCache.get(imageUri) || '#1a1a1a'; } // Never return null - always provide immediate fallback return '#1a1a1a'; }); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const lastSetColorRef = useRef(dominantColor); const safelySetColor = useCallback((color: string) => { if (lastSetColorRef.current !== color) { lastSetColorRef.current = color; setDominantColor(color); } }, []); const extractColor = useCallback(async (uri: string) => { if (!uri) { safelySetColor('#1a1a1a'); setLoading(false); return; } // Check cache first if (colorCache.has(uri)) { const cachedColor = colorCache.get(uri)!; safelySetColor(cachedColor); setLoading(false); return; } try { setLoading(true); setError(null); // Use highest quality for best color accuracy const fastResult: ImageColorsResult = await getColors(uri, { fallback: '#1a1a1a', cache: true, key: uri, quality: 'highest', // Best quality for accurate colors pixelSpacing: 1, // Sample every pixel for best accuracy (Android only) }); const fastColor = selectBestColor(fastResult); colorCache.set(uri, fastColor); // Cache fast color to avoid flicker safelySetColor(fastColor); setLoading(false); // Since we're already using highest quality, no need for refinement } catch (err) { if (__DEV__) console.warn('[useDominantColor] Failed to extract color:', err); setError(err instanceof Error ? err.message : 'Failed to extract color'); const fallbackColor = '#1a1a1a'; colorCache.set(uri, fallbackColor); // Cache fallback to avoid repeated failures safelySetColor(fallbackColor); } finally { // loading already set to false } }, []); useEffect(() => { if (imageUri) { // If we have a cached color, use it immediately, but still extract in background for accuracy if (colorCache.has(imageUri)) { safelySetColor(colorCache.get(imageUri)!); setLoading(false); } else { // No cache, extract color extractColor(imageUri); } } else { safelySetColor('#1a1a1a'); setLoading(false); setError(null); } }, [imageUri, extractColor, safelySetColor]); return { dominantColor, loading, error, }; }; export default useDominantColor;