mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at verify-code 8.6 kB view raw
1import React, {useCallback, useEffect} from 'react' 2import { 3 View, 4 StyleSheet, 5 Image as RNImage, 6 AccessibilityInfo, 7 useColorScheme, 8} from 'react-native' 9import * as SplashScreen from 'expo-splash-screen' 10import {Image} from 'expo-image' 11import Animated, { 12 interpolate, 13 runOnJS, 14 useAnimatedStyle, 15 useSharedValue, 16 withTiming, 17 Easing, 18} from 'react-native-reanimated' 19import MaskedView from '@react-native-masked-view/masked-view' 20import {useSafeAreaInsets} from 'react-native-safe-area-context' 21import Svg, {Path, SvgProps} from 'react-native-svg' 22 23import {isAndroid} from '#/platform/detection' 24import {Logotype} from '#/view/icons/Logotype' 25 26// @ts-ignore 27import splashImagePointer from '../assets/splash.png' 28// @ts-ignore 29import darkSplashImagePointer from '../assets/splash-dark.png' 30const splashImageUri = RNImage.resolveAssetSource(splashImagePointer).uri 31const darkSplashImageUri = RNImage.resolveAssetSource( 32 darkSplashImagePointer, 33).uri 34 35export const Logo = React.forwardRef(function LogoImpl(props: SvgProps, ref) { 36 const width = 1000 37 const height = width * (67 / 64) 38 return ( 39 <Svg 40 fill="none" 41 // @ts-ignore it's fiiiiine 42 ref={ref} 43 viewBox="0 0 64 66" 44 style={[{width, height}, props.style]}> 45 <Path 46 fill={props.fill || '#fff'} 47 d="M13.873 3.77C21.21 9.243 29.103 20.342 32 26.3v15.732c0-.335-.13.043-.41.858-1.512 4.414-7.418 21.642-20.923 7.87-7.111-7.252-3.819-14.503 9.125-16.692-7.405 1.252-15.73-.817-18.014-8.93C1.12 22.804 0 8.431 0 6.488 0-3.237 8.579-.18 13.873 3.77ZM50.127 3.77C42.79 9.243 34.897 20.342 32 26.3v15.732c0-.335.13.043.41.858 1.512 4.414 7.418 21.642 20.923 7.87 7.111-7.252 3.819-14.503-9.125-16.692 7.405 1.252 15.73-.817 18.014-8.93C62.88 22.804 64 8.431 64 6.488 64-3.237 55.422-.18 50.127 3.77Z" 48 /> 49 </Svg> 50 ) 51}) 52 53type Props = { 54 isReady: boolean 55} 56 57const AnimatedLogo = Animated.createAnimatedComponent(Logo) 58 59export function Splash(props: React.PropsWithChildren<Props>) { 60 const insets = useSafeAreaInsets() 61 const intro = useSharedValue(0) 62 const outroLogo = useSharedValue(0) 63 const outroApp = useSharedValue(0) 64 const outroAppOpacity = useSharedValue(0) 65 const [isAnimationComplete, setIsAnimationComplete] = React.useState(false) 66 const [isImageLoaded, setIsImageLoaded] = React.useState(false) 67 const [isLayoutReady, setIsLayoutReady] = React.useState(false) 68 const [reduceMotion, setReduceMotion] = React.useState<boolean | undefined>( 69 false, 70 ) 71 const isReady = 72 props.isReady && 73 isImageLoaded && 74 isLayoutReady && 75 reduceMotion !== undefined 76 77 const colorScheme = useColorScheme() 78 const isDarkMode = colorScheme === 'dark' 79 80 const logoAnimation = useAnimatedStyle(() => { 81 return { 82 transform: [ 83 { 84 scale: interpolate(intro.value, [0, 1], [0.8, 1], 'clamp'), 85 }, 86 { 87 scale: interpolate( 88 outroLogo.value, 89 [0, 0.08, 1], 90 [1, 0.8, 500], 91 'clamp', 92 ), 93 }, 94 ], 95 opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'), 96 } 97 }) 98 const bottomLogoAnimation = useAnimatedStyle(() => { 99 return { 100 opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'), 101 } 102 }) 103 const reducedLogoAnimation = useAnimatedStyle(() => { 104 return { 105 transform: [ 106 { 107 scale: interpolate(intro.value, [0, 1], [0.8, 1], 'clamp'), 108 }, 109 ], 110 opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'), 111 } 112 }) 113 114 const logoWrapperAnimation = useAnimatedStyle(() => { 115 return { 116 opacity: interpolate( 117 outroAppOpacity.value, 118 [0, 0.1, 0.2, 1], 119 [1, 1, 0, 0], 120 'clamp', 121 ), 122 } 123 }) 124 125 const appAnimation = useAnimatedStyle(() => { 126 return { 127 transform: [ 128 { 129 scale: interpolate(outroApp.value, [0, 1], [1.1, 1], 'clamp'), 130 }, 131 ], 132 opacity: interpolate( 133 outroAppOpacity.value, 134 [0, 0.1, 0.2, 1], 135 [0, 0, 1, 1], 136 'clamp', 137 ), 138 } 139 }) 140 141 const onFinish = useCallback(() => setIsAnimationComplete(true), []) 142 const onLayout = useCallback(() => setIsLayoutReady(true), []) 143 const onLoadEnd = useCallback(() => setIsImageLoaded(true), []) 144 145 useEffect(() => { 146 if (isReady) { 147 SplashScreen.hideAsync() 148 .then(() => { 149 intro.value = withTiming( 150 1, 151 {duration: 400, easing: Easing.out(Easing.cubic)}, 152 async () => { 153 // set these values to check animation at specific point 154 // outroLogo.value = 0.1 155 // outroApp.value = 0.1 156 outroLogo.value = withTiming( 157 1, 158 {duration: 1200, easing: Easing.in(Easing.cubic)}, 159 () => { 160 runOnJS(onFinish)() 161 }, 162 ) 163 outroApp.value = withTiming(1, { 164 duration: 1200, 165 easing: Easing.inOut(Easing.cubic), 166 }) 167 outroAppOpacity.value = withTiming(1, { 168 duration: 1200, 169 easing: Easing.in(Easing.cubic), 170 }) 171 }, 172 ) 173 }) 174 .catch(() => {}) 175 } 176 }, [onFinish, intro, outroLogo, outroApp, outroAppOpacity, isReady]) 177 178 useEffect(() => { 179 AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotion) 180 }, []) 181 182 const logoAnimations = 183 reduceMotion === true ? reducedLogoAnimation : logoAnimation 184 // special off-spec color for dark mode 185 const logoBg = isDarkMode ? '#0F1824' : '#fff' 186 187 return ( 188 <View style={{flex: 1}} onLayout={onLayout}> 189 {!isAnimationComplete && ( 190 <View style={StyleSheet.absoluteFillObject}> 191 <Image 192 accessibilityIgnoresInvertColors 193 onLoadEnd={onLoadEnd} 194 source={{uri: isDarkMode ? darkSplashImageUri : splashImageUri}} 195 style={StyleSheet.absoluteFillObject} 196 /> 197 198 <Animated.View 199 style={[ 200 bottomLogoAnimation, 201 { 202 position: 'absolute', 203 bottom: insets.bottom + 40, 204 left: 0, 205 right: 0, 206 alignItems: 'center', 207 justifyContent: 'center', 208 opacity: 0, 209 }, 210 ]}> 211 <Logotype fill="#fff" width={90} /> 212 </Animated.View> 213 </View> 214 )} 215 216 {isReady && 217 (isAndroid || reduceMotion === true ? ( 218 // Use a simple fade on older versions of android (work around a bug) 219 <> 220 <Animated.View style={[{flex: 1}, appAnimation]}> 221 {props.children} 222 </Animated.View> 223 224 {!isAnimationComplete && ( 225 <Animated.View 226 style={[ 227 StyleSheet.absoluteFillObject, 228 logoWrapperAnimation, 229 { 230 flex: 1, 231 justifyContent: 'center', 232 alignItems: 'center', 233 transform: [{translateY: -(insets.top / 2)}, {scale: 0.1}], // scale from 1000px to 100px 234 }, 235 ]}> 236 <AnimatedLogo 237 fill={logoBg} 238 style={[{opacity: 0}, logoAnimations]} 239 /> 240 </Animated.View> 241 )} 242 </> 243 ) : ( 244 <MaskedView 245 style={[StyleSheet.absoluteFillObject]} 246 maskElement={ 247 <Animated.View 248 style={[ 249 { 250 // Transparent background because mask is based off alpha channel. 251 backgroundColor: 'transparent', 252 flex: 1, 253 justifyContent: 'center', 254 alignItems: 'center', 255 transform: [{translateY: -(insets.top / 2)}, {scale: 0.1}], // scale from 1000px to 100px 256 }, 257 ]}> 258 <AnimatedLogo fill={logoBg} style={[logoAnimations]} /> 259 </Animated.View> 260 }> 261 {!isAnimationComplete && ( 262 <View 263 style={[ 264 StyleSheet.absoluteFillObject, 265 { 266 backgroundColor: logoBg, 267 }, 268 ]} 269 /> 270 )} 271 <Animated.View style={[{flex: 1}, appAnimation]}> 272 {props.children} 273 </Animated.View> 274 </MaskedView> 275 ))} 276 </View> 277 ) 278}