import React, { useState, useMemo, FC, useEffect, useRef, Suspense } from 'react'; import { View, LayoutChangeEvent, Platform, StyleProp, ViewStyle } from 'react-native'; import Animated, { useSharedValue, withTiming, Easing, type SharedValue } from 'react-native-reanimated'; import type { AnimatedGlowProps, GlowLayerConfig, Layout, PresetConfig, GlowEvent, GlowState, GlowConfig } from './animated-glow/types'; import { SkiaRoot } from './animated-glow/SkiaRoot'; export type { PresetConfig, GlowLayerConfig, AnimatedGlowProps, GlowEvent, GlowState, GlowConfig }; const isObject = (item: any) => (item && typeof item === 'object' && !Array.isArray(item)); const mergeDeep = (target: any, source: any): any => { const output = { ...target }; if (isObject(target) && isObject(source)) { Object.keys(source).forEach(key => { if (key === 'glowLayers' && Array.isArray(source[key]) && Array.isArray(target[key])) { const mergedLayers = target[key].map((layer, index) => source[key][index] ? { ...layer, ...source[key][index] } : layer ); if (source[key].length > target[key].length) { mergedLayers.push(...source[key].slice(target[key].length)); } output[key] = mergedLayers; } else if (isObject(source[key]) && key in target && isObject(target[key])) { output[key] = mergeDeep(target[key], source[key]); } else { output[key] = source[key]; } }); } return output; }; const AnimatedGlow: FC = (props) => { const { preset = {}, states: statesProp, initialState = 'default', children, style, isVisible = true, activeState: activeStateProp, wrapperStyle: wrapperStyleProp, ...overrideProps } = props; const [layout, setLayout] = useState({ width: 0, height: 0 }); const [hasLaidOut, setHasLaidOut] = useState(false); const activeState = activeStateProp ?? initialState; const prevActiveState = useRef(activeState); const skiaOpacity = useSharedValue(0); const animationProgress = useSharedValue(0); const fromConfigSV = useSharedValue({}); const toConfigSV = useSharedValue({}); const states = useMemo(() => statesProp || preset.states || [], [statesProp, preset.states]); const targetConfig = useMemo((): GlowConfig => { const allStates = statesProp || preset.states || []; const defaultState = allStates.find(s => s.name === 'default')?.preset || {}; const legacyBase = { ...preset, ...overrideProps }; delete legacyBase.metadata; delete legacyBase.states; const baseConfig = mergeDeep(defaultState, legacyBase); const stateOverride = allStates.find(s => s.name === activeState)?.preset || {}; return mergeDeep(baseConfig, stateOverride); }, [preset, overrideProps, statesProp, activeState]); useEffect(() => { if (animationProgress.value === 0 && !fromConfigSV.value.cornerRadius) { fromConfigSV.value = targetConfig; toConfigSV.value = targetConfig; animationProgress.value = 1; prevActiveState.current = activeState; return; } let transition = states.find(s => s.name === activeState)?.transition ?? 0; if (activeState === 'default' && prevActiveState.current !== 'default') { transition = states.find(s => s.name === prevActiveState.current)?.transition ?? transition; } fromConfigSV.value = toConfigSV.value; toConfigSV.value = targetConfig; if (transition > 0) { animationProgress.value = 0; animationProgress.value = withTiming(1, { duration: transition, easing: Easing.out(Easing.quad) }); } else { animationProgress.value = 1; } prevActiveState.current = activeState; }, [targetConfig, activeState, states]); const { cornerRadius = 10, outlineWidth = 2, borderColor = 'white', backgroundColor } = targetConfig; const hasAnimatedBorder = Array.isArray(borderColor) && borderColor.length > 1; const hasGlowLayers = (targetConfig.glowLayers?.length ?? 0) > 0; const useSkiaRenderer = useMemo(() => hasGlowLayers || hasAnimatedBorder, [hasGlowLayers, hasAnimatedBorder]); const wrapperStyle = useMemo>(() => [ { backgroundColor: useSkiaRenderer ? 'transparent' : backgroundColor, borderWidth: useSkiaRenderer ? 0 : outlineWidth, borderColor: useSkiaRenderer ? 'transparent' : (Array.isArray(borderColor) ? borderColor[0] : borderColor), borderRadius: cornerRadius, overflow: 'hidden', }, wrapperStyleProp, ], [useSkiaRenderer, outlineWidth, borderColor, cornerRadius, backgroundColor, wrapperStyleProp]); const shouldRenderSkia = useSkiaRenderer && hasLaidOut && isVisible; useEffect(() => { skiaOpacity.value = withTiming(shouldRenderSkia ? 1 : 0, { duration: 300 }); }, [shouldRenderSkia]); return ( { const l = e.nativeEvent.layout; if (l.width !== layout.width || l.height !== layout.height) { setLayout({ width: l.width, height: l.height }); } if (!hasLaidOut) setHasLaidOut(true); }} > {shouldRenderSkia && ( )} {children} ); }; export default AnimatedGlow;