mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
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}