forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import React, {useCallback, useEffect} from 'react'
2import {
3 AccessibilityInfo,
4 Image as RNImage,
5 StyleSheet,
6 useColorScheme,
7 View,
8} from 'react-native'
9import Animated, {
10 Easing,
11 interpolate,
12 runOnJS,
13 useAnimatedStyle,
14 useSharedValue,
15 withTiming,
16} from 'react-native-reanimated'
17import {useSafeAreaInsets} from 'react-native-safe-area-context'
18import Svg, {Path, type SvgProps} from 'react-native-svg'
19import {Image} from 'expo-image'
20import * as SplashScreen from 'expo-splash-screen'
21
22import {Logotype} from '#/view/icons/Logotype'
23// @ts-ignore
24import splashImagePointer from '../assets/splash.png'
25// @ts-ignore
26import darkSplashImagePointer from '../assets/splash-dark.png'
27const splashImageUri = RNImage.resolveAssetSource(splashImagePointer).uri
28const darkSplashImageUri = RNImage.resolveAssetSource(
29 darkSplashImagePointer,
30).uri
31
32export const Logo = React.forwardRef(function LogoImpl(props: SvgProps, ref) {
33 const width = 1000
34 const height = width * (67 / 64)
35 return (
36 <Svg
37 fill="none"
38 // @ts-ignore it's fiiiiine
39 ref={ref}
40 viewBox="0 0 64 66"
41 style={[{width, height}, props.style]}>
42 <Path
43 fill={props.fill || '#fff'}
44 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"
45 />
46 </Svg>
47 )
48})
49
50type Props = {
51 isReady: boolean
52}
53
54export function Splash(props: React.PropsWithChildren<Props>) {
55 'use no memo'
56 const insets = useSafeAreaInsets()
57 const intro = useSharedValue(0)
58 const outroLogo = useSharedValue(0)
59 const outroApp = useSharedValue(0)
60 const outroAppOpacity = useSharedValue(0)
61 const [isAnimationComplete, setIsAnimationComplete] = React.useState(false)
62 const [isImageLoaded, setIsImageLoaded] = React.useState(false)
63 const [isLayoutReady, setIsLayoutReady] = React.useState(false)
64 const [reduceMotion, setReduceMotion] = React.useState<boolean | undefined>(
65 false,
66 )
67 const isReady =
68 props.isReady &&
69 isImageLoaded &&
70 isLayoutReady &&
71 reduceMotion !== undefined
72
73 const colorScheme = useColorScheme()
74 const isDarkMode = colorScheme === 'dark'
75
76 const logoAnimation = useAnimatedStyle(() => {
77 return {
78 transform: [
79 {
80 scale: interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp'),
81 },
82 {
83 scale: interpolate(
84 outroLogo.get(),
85 [0, 0.08, 1],
86 [1, 0.8, 500],
87 'clamp',
88 ),
89 },
90 ],
91 opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
92 }
93 })
94 const bottomLogoAnimation = useAnimatedStyle(() => {
95 return {
96 opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
97 }
98 })
99 const reducedLogoAnimation = useAnimatedStyle(() => {
100 return {
101 transform: [
102 {
103 scale: interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp'),
104 },
105 ],
106 opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
107 }
108 })
109
110 const logoWrapperAnimation = useAnimatedStyle(() => {
111 return {
112 opacity: interpolate(
113 outroAppOpacity.get(),
114 [0, 0.1, 0.2, 1],
115 [1, 1, 0, 0],
116 'clamp',
117 ),
118 }
119 })
120
121 const appAnimation = useAnimatedStyle(() => {
122 return {
123 transform: [
124 {
125 scale: interpolate(outroApp.get(), [0, 1], [1.1, 1], 'clamp'),
126 },
127 ],
128 opacity: interpolate(
129 outroAppOpacity.get(),
130 [0, 0.1, 0.2, 1],
131 [0, 0, 1, 1],
132 'clamp',
133 ),
134 }
135 })
136
137 const onFinish = useCallback(() => setIsAnimationComplete(true), [])
138 const onLayout = useCallback(() => setIsLayoutReady(true), [])
139 const onLoadEnd = useCallback(() => setIsImageLoaded(true), [])
140
141 useEffect(() => {
142 if (isReady) {
143 SplashScreen.hideAsync()
144 .then(() => {
145 intro.set(() =>
146 withTiming(
147 1,
148 {duration: 400, easing: Easing.out(Easing.cubic)},
149 async () => {
150 // set these values to check animation at specific point
151 outroLogo.set(() =>
152 withTiming(
153 1,
154 {duration: 1200, easing: Easing.in(Easing.cubic)},
155 () => {
156 runOnJS(onFinish)()
157 },
158 ),
159 )
160 outroApp.set(() =>
161 withTiming(1, {
162 duration: 1200,
163 easing: Easing.inOut(Easing.cubic),
164 }),
165 )
166 outroAppOpacity.set(() =>
167 withTiming(1, {
168 duration: 1200,
169 easing: Easing.in(Easing.cubic),
170 }),
171 )
172 },
173 ),
174 )
175 })
176 .catch(() => {})
177 }
178 }, [onFinish, intro, outroLogo, outroApp, outroAppOpacity, isReady])
179
180 useEffect(() => {
181 AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotion)
182 }, [])
183
184 const logoAnimations =
185 reduceMotion === true ? reducedLogoAnimation : logoAnimation
186 // special off-spec color for dark mode
187 const logoBg = isDarkMode ? '#0F1824' : '#fff'
188
189 return (
190 <View style={{flex: 1}} onLayout={onLayout}>
191 {!isAnimationComplete && (
192 <View style={StyleSheet.absoluteFillObject}>
193 <Image
194 accessibilityIgnoresInvertColors
195 onLoadEnd={onLoadEnd}
196 source={{uri: isDarkMode ? darkSplashImageUri : splashImageUri}}
197 style={StyleSheet.absoluteFillObject}
198 />
199
200 <Animated.View
201 style={[
202 bottomLogoAnimation,
203 {
204 position: 'absolute',
205 bottom: insets.bottom + 40,
206 left: 0,
207 right: 0,
208 alignItems: 'center',
209 justifyContent: 'center',
210 opacity: 0,
211 },
212 ]}>
213 <Logotype fill="#fff" width={90} />
214 </Animated.View>
215 </View>
216 )}
217
218 {isReady && (
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 <Animated.View style={[logoAnimations]}>
237 <Logo fill={logoBg} />
238 </Animated.View>
239 </Animated.View>
240 )}
241 </>
242 )}
243 </View>
244 )
245}