From 7cbff868c31aca2392fbead1308e14c4defc1315 Mon Sep 17 00:00:00 2001 From: Matt Rabe Date: Sun, 8 Mar 2026 21:01:37 -1000 Subject: [PATCH] Better pulsating IconFilter --- apps/mobile/src/components/IconFilter.tsx | 92 ++++++----------------- apps/mobile/src/utils/color.ts | 20 +++++ 2 files changed, 42 insertions(+), 70 deletions(-) create mode 100644 apps/mobile/src/utils/color.ts diff --git a/apps/mobile/src/components/IconFilter.tsx b/apps/mobile/src/components/IconFilter.tsx index 33160ab..1dc154e 100644 --- a/apps/mobile/src/components/IconFilter.tsx +++ b/apps/mobile/src/components/IconFilter.tsx @@ -1,9 +1,11 @@ import React, { useEffect, useRef, useState } from 'react' -import { Animated, StyleSheet, TouchableOpacity, View } from 'react-native' +import { Animated, TouchableOpacity } from 'react-native' import { Ionicons } from '@expo/vector-icons' import { palette } from '../styles' +import { hexToRgb } from '../utils/color' + export function IconFilter({ onPress, disabled = false, @@ -17,29 +19,15 @@ export function IconFilter({ isPulsating?: boolean, testID?: string, }) { + const defaultIconColor = color || palette.textPrimary + const pulseAnim = useRef(new Animated.Value(0)).current const iconColorAnim = useRef(new Animated.Value(0)).current - const [iconColor, setIconColor] = useState('#fff') + const [iconColor, setIconColor] = useState(defaultIconColor) useEffect(() => { if (isPulsating) { - // Pulsate the background circle - Animated.loop( - Animated.sequence([ - Animated.timing(pulseAnim, { - toValue: 1, - duration: 1000, - useNativeDriver: true, - }), - Animated.timing(pulseAnim, { - toValue: 0, - duration: 1000, - useNativeDriver: true, - }), - ]) - ).start() - - // Pulsate the icon color (white to black) + // Pulsate the icon color const colorAnimation = Animated.loop( Animated.sequence([ Animated.timing(iconColorAnim, { @@ -57,12 +45,14 @@ export function IconFilter({ colorAnimation.start() + const targetRgb = hexToRgb(defaultIconColor) + // Listen to color animation changes const listenerId = iconColorAnim.addListener(({ value }) => { - // Interpolate from white (#fff) to black (#000) - const r = Math.round(255 * (1 - value)) - const g = Math.round(255 * (1 - value)) - const b = Math.round(255 * (1 - value)) + // Interpolate from white (#fff) to defaultIconColor + const r = Math.round(255 + (targetRgb.r - 255) * value) + const g = Math.round(255 + (targetRgb.g - 255) * value) + const b = Math.round(255 + (targetRgb.b - 255) * value) setIconColor(`rgb(${r}, ${g}, ${b})`) }) @@ -73,47 +63,19 @@ export function IconFilter({ } else { pulseAnim.setValue(0) iconColorAnim.setValue(0) - setIconColor('#fff') + setIconColor(defaultIconColor) } - }, [isPulsating, pulseAnim, iconColorAnim]) + }, [isPulsating, pulseAnim, iconColorAnim, defaultIconColor]) - const backgroundOpacity = pulseAnim.interpolate({ - inputRange: [0, 1], - outputRange: [0, 1], - }) - - const backgroundScale = pulseAnim.interpolate({ - inputRange: [0, 1], - outputRange: [0.8, 1], - }) - - const icon = isPulsating ? ( - - + - - - - ) : ( - ) if (onPress) { @@ -134,13 +96,3 @@ export function IconFilter({ return icon } - -const styles = StyleSheet.create({ - pulsatingBackground: { - position: 'absolute', - width: 32, - height: 32, - borderRadius: 16, - backgroundColor: '#fff', - }, -}) diff --git a/apps/mobile/src/utils/color.ts b/apps/mobile/src/utils/color.ts new file mode 100644 index 0000000..989fefa --- /dev/null +++ b/apps/mobile/src/utils/color.ts @@ -0,0 +1,20 @@ +export function hexToRgb(hex: string) { + try { + let c = hex.replace('#', '') + if (c.length === 3) { + c = c.split('').map(x => x + x).join('') + } + const num = parseInt(c, 16) + + if (isNaN(num)) throw new Error('Invalid hex') + + return { + r: (num >> 16) & 255, + g: (num >> 8) & 255, + b: num & 255, + } + } catch (e) { + // Fallback to textPrimary rgb(31, 41, 51) + return { r: 31, g: 41, b: 51 } + } +}