diff --git a/app.config.js b/app.config.js index 6e43ab83f8..c2eab958ea 100644 --- a/app.config.js +++ b/app.config.js @@ -310,15 +310,17 @@ module.exports = function (_config) { [ 'expo-splash-screen', { - enableFullScreenImage_legacy: true, // iOS only - backgroundColor: '#EEF5FF', // based on illustration - image: './assets/splash/splash-mobile.png', - resizeMode: 'cover', - dark: { + ios: { enableFullScreenImage_legacy: true, // iOS only - backgroundColor: '#446DA9', // based on illustration - image: './assets/splash/splash-mobile-dark.png', + backgroundColor: '#A8CCFF', // primary_200 + image: './assets/splash/splash.png', resizeMode: 'cover', + dark: { + enableFullScreenImage_legacy: true, // iOS only + backgroundColor: '#00398A', // primary_800 + image: './assets/splash/splash-dark.png', + resizeMode: 'cover', + }, }, android: { backgroundColor: '#A8CCFF', // primary_200 diff --git a/assets/splash/splash-mobile-dark.png b/assets/splash/illustration-mobile-dark.png similarity index 100% rename from assets/splash/splash-mobile-dark.png rename to assets/splash/illustration-mobile-dark.png diff --git a/assets/splash/splash-mobile.png b/assets/splash/illustration-mobile.png similarity index 100% rename from assets/splash/splash-mobile.png rename to assets/splash/illustration-mobile.png diff --git a/assets/splash/splash-dark.png b/assets/splash/splash-dark.png new file mode 100644 index 0000000000..95176b3ff4 Binary files /dev/null and b/assets/splash/splash-dark.png differ diff --git a/assets/splash/splash.png b/assets/splash/splash.png new file mode 100644 index 0000000000..05a7392b37 Binary files /dev/null and b/assets/splash/splash.png differ diff --git a/src/Splash.tsx b/src/Splash.tsx index 7fc09d7ab9..7a99d36891 100644 --- a/src/Splash.tsx +++ b/src/Splash.tsx @@ -9,30 +9,55 @@ import { import Animated, { Easing, interpolate, + runOnJS, useAnimatedStyle, useSharedValue, withTiming, } from 'react-native-reanimated' +import {useSafeAreaInsets} from 'react-native-safe-area-context' +import Svg, {Path, type SvgProps} from 'react-native-svg' import {Image} from 'expo-image' import * as SplashScreen from 'expo-splash-screen' +import {Logotype} from '#/view/icons/Logotype' // @ts-ignore -import splashImagePointer from '../assets/splash/splash-mobile.png' +import splashImagePointer from '../assets/splash/splash.png' // @ts-ignore -import darkSplashImagePointer from '../assets/splash/splash-mobile-dark.png' +import darkSplashImagePointer from '../assets/splash/splash-dark.png' const splashImageUri = RNImage.resolveAssetSource(splashImagePointer).uri const darkSplashImageUri = RNImage.resolveAssetSource( darkSplashImagePointer, ).uri +export const Logo = React.forwardRef(function LogoImpl(props: SvgProps, ref) { + const width = 1000 + const height = width * (67 / 64) + return ( + + + + ) +}) + type Props = { isReady: boolean } export function Splash(props: React.PropsWithChildren) { 'use no memo' + const insets = useSafeAreaInsets() + const intro = useSharedValue(0) + const outroLogo = useSharedValue(0) + const outroApp = useSharedValue(0) const outroAppOpacity = useSharedValue(0) - const colorScheme = useColorScheme() const [isAnimationComplete, setIsAnimationComplete] = React.useState(false) const [isImageLoaded, setIsImageLoaded] = React.useState(false) const [isLayoutReady, setIsLayoutReady] = React.useState(false) @@ -44,10 +69,62 @@ export function Splash(props: React.PropsWithChildren) { isImageLoaded && isLayoutReady && reduceMotion !== undefined + + const colorScheme = useColorScheme() const isDarkMode = colorScheme === 'dark' + const logoAnimation = useAnimatedStyle(() => { + return { + transform: [ + { + scale: interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp'), + }, + { + scale: interpolate( + outroLogo.get(), + [0, 0.08, 1], + [1, 0.8, 500], + 'clamp', + ), + }, + ], + opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'), + } + }) + const bottomLogoAnimation = useAnimatedStyle(() => { + return { + opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'), + } + }) + const reducedLogoAnimation = useAnimatedStyle(() => { + return { + transform: [ + { + scale: interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp'), + }, + ], + opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'), + } + }) + + const logoWrapperAnimation = useAnimatedStyle(() => { + return { + opacity: interpolate( + outroAppOpacity.get(), + [0, 0.1, 0.2, 1], + [1, 1, 0, 0], + 'clamp', + ), + } + }) + const appAnimation = useAnimatedStyle(() => { return { + transform: [ + { + scale: interpolate(outroApp.get(), [0, 1], [1.1, 1], 'clamp'), + }, + ], opacity: interpolate( outroAppOpacity.get(), [0, 0.1, 0.2, 1], @@ -65,21 +142,50 @@ export function Splash(props: React.PropsWithChildren) { if (isReady) { SplashScreen.hideAsync() .then(() => { - outroAppOpacity.set(() => - withTiming(1, { - duration: 1200, - easing: Easing.in(Easing.cubic), - }), + intro.set(() => + withTiming( + 1, + {duration: 400, easing: Easing.out(Easing.cubic)}, + async () => { + // set these values to check animation at specific point + outroLogo.set(() => + withTiming( + 1, + {duration: 1200, easing: Easing.in(Easing.cubic)}, + () => { + runOnJS(onFinish)() + }, + ), + ) + outroApp.set(() => + withTiming(1, { + duration: 1200, + easing: Easing.inOut(Easing.cubic), + }), + ) + outroAppOpacity.set(() => + withTiming(1, { + duration: 1200, + easing: Easing.in(Easing.cubic), + }), + ) + }, + ), ) }) .catch(() => {}) } - }, [onFinish, outroAppOpacity, isReady]) + }, [onFinish, intro, outroLogo, outroApp, outroAppOpacity, isReady]) useEffect(() => { AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotion) }, []) + const logoAnimations = + reduceMotion === true ? reducedLogoAnimation : logoAnimation + // special off-spec color for dark mode + const logoBg = isDarkMode ? '#0F1824' : '#fff' + return ( {!isAnimationComplete && ( @@ -90,6 +196,22 @@ export function Splash(props: React.PropsWithChildren) { source={{uri: isDarkMode ? darkSplashImageUri : splashImageUri}} style={StyleSheet.absoluteFillObject} /> + + + + )} @@ -98,6 +220,24 @@ export function Splash(props: React.PropsWithChildren) { {props.children} + + {!isAnimationComplete && ( + + + + + + )} )} diff --git a/src/view/com/auth/SplashScreen.tsx b/src/view/com/auth/SplashScreen.tsx index 77973ed3d7..cc44776969 100644 --- a/src/view/com/auth/SplashScreen.tsx +++ b/src/view/com/auth/SplashScreen.tsx @@ -11,9 +11,9 @@ import {Logotype} from '#/view/icons/Logotype' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' // @ts-ignore -import splashImagePointer from '../../../../assets/splash/splash-mobile.png' +import splashImagePointer from '../../../../assets/splash/illustration-mobile.png' // @ts-ignore -import darkSplashImagePointer from '../../../../assets/splash/splash-mobile-dark.png' +import darkSplashImagePointer from '../../../../assets/splash/illustration-mobile-dark.png' const splashImageUri = RNImage.resolveAssetSource(splashImagePointer).uri const darkSplashImageUri = RNImage.resolveAssetSource( darkSplashImagePointer, @@ -78,7 +78,7 @@ export const SplashScreen = ({ + style={[a.px_5xl, a.gap_md, a.pb_sm]}>