Skip to content

Commit

Permalink
animate onboarding
Browse files Browse the repository at this point in the history
  • Loading branch information
tmgrask committed Oct 4, 2024
1 parent 10ea926 commit d014fb5
Show file tree
Hide file tree
Showing 14 changed files with 891 additions and 49 deletions.
Binary file added assets/images/orbs-world.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/view-of-earth.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
207 changes: 207 additions & 0 deletions src/app/(app)/intro.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import {
Blur,
Canvas,
Circle,
ColorMatrix,
Group,
Image,
ImageSVG,
Paint,
RadialGradient,
Shadow,
fitbox,
rect,
useImage,
useSVG,
vec,
} from "@shopify/react-native-skia";
import React from "react";
import { View, useWindowDimensions } from "react-native";
import {
Easing,
runOnJS,
useDerivedValue,
useSharedValue,
withDelay,
withSequence,
withSpring,
withTiming,
} from "react-native-reanimated";

import { palette, sharedStyles as ss } from "@/src/styles";
import { router } from "expo-router";
import { useSafeAreaInsets } from "react-native-safe-area-context";

function Orb({ cx, cy, r }: { cx: number; cy: number; r: number }) {
const win = useWindowDimensions();

const fullHeight = win.height; // + insets.top + insets.bottom;

const radius = useSharedValue(0);
const shadowPos = useDerivedValue(() => {
return radius.value / 5;
});
const shadowNeg = useDerivedValue(() => {
return -radius.value / 5;
});
const shadowBlur = (r / 80) * 10;

const computedelayMs = fullHeight - cy + cx * 4;

React.useEffect(() => {
radius.value = withDelay(
computedelayMs,
withSpring(r, {
mass: 1.2,
damping: 10,
stiffness: 100,
restDisplacementThreshold: 0.01,
restSpeedThreshold: 2,
}),
);
}, []);

return (
<Circle cx={cx} cy={cy} r={radius}>
<Shadow
dx={shadowPos}
dy={shadowPos}
blur={shadowBlur}
color={palette.purple}
inner
/>
<Shadow
dx={shadowNeg}
dy={shadowNeg}
blur={shadowBlur}
color={palette.blue}
inner
/>
<RadialGradient
c={vec(cx, cy)}
r={radius}
colors={[palette.redShade3, palette.black]}
/>
<Blur blur={Math.floor((cy * 2) / fullHeight) * 1} />
</Circle>
);
}

export default function IntroScreen() {
const win = useWindowDimensions();
const insets = useSafeAreaInsets();

const fullHeight = win.height + insets.top + insets.bottom;
const fullWidth = win.width + insets.left + insets.right;

const opacity = useSharedValue(0);

const backgroundEarthPng = useImage(
require("@/assets/images/view-of-earth.png"),
);
const backgroundPanX = useSharedValue(0);
const orbsPanTransform = useDerivedValue(() => {
return [{ translateX: backgroundPanX.value }];
});

const conduitWordMarkSvg = useSVG(
require("@/assets/images/psiphon-conduit-wordmark.svg"),
);
const originalWordMarkWidth = 141;
const originalWordMarkHeight = 44;
const wordMarkSrc = rect(
0,
0,
originalWordMarkWidth,
originalWordMarkHeight,
);
const wordMarkDst = rect(
fullWidth * 0.1,
fullHeight * 0.1,
fullWidth * 0.8,
fullHeight * 0.2,
);
const wordMarkResizeTransform = fitbox("contain", wordMarkSrc, wordMarkDst);

React.useEffect(() => {
opacity.value = withSequence(
withTiming(1, { duration: 1000 }),
withDelay(
3000,
withTiming(0, { duration: 1000 }, () => {
runOnJS(router.replace)("/(app)/onboarding");
}),
),
);
backgroundPanX.value = withTiming(-win.width, {
duration: 5000,
easing: Easing.inOut(Easing.ease),
});
}, []);

const opacityMatrix = useDerivedValue(() => {
// prettier-ignore
return [
// R, G, B, A, Bias
1, 0, 0, 0, 0,
0, 1, 0, 0, 0,
0, 0, 1, 0, 0,
0, 0, 0, opacity.value, 0,
];
});

if (!backgroundEarthPng) {
return null;
}

const bgWidth = win.width * 2;
const bgHeight =
(bgWidth / backgroundEarthPng.width()) * backgroundEarthPng.height();

return (
<View
style={{
width: fullWidth,
height: fullHeight,
}}
>
<Canvas style={[ss.flex]}>
<Image
image={backgroundEarthPng}
fit="fitHeight"
x={backgroundPanX}
y={0}
width={bgWidth}
height={bgHeight}
opacity={opacity}
/>
<Group
layer={
<Paint>
<ColorMatrix matrix={opacityMatrix} />
</Paint>
}
>
<Group transform={wordMarkResizeTransform}>
<ImageSVG svg={conduitWordMarkSvg} x={0} y={0} />
</Group>
</Group>
<Group transform={orbsPanTransform} opacity={opacity}>
<Orb cx={fullWidth * 0.2} cy={fullHeight * 0.9} r={50} />
<Orb cx={fullWidth * 0.1} cy={fullHeight * 0.66} r={20} />
<Orb cx={fullWidth * 0.42} cy={fullHeight * 0.61} r={15} />
<Orb cx={fullWidth * 0.4} cy={fullHeight * 0.75} r={35} />
<Orb cx={fullWidth * 0.65} cy={fullHeight * 0.95} r={59} />
<Orb cx={fullWidth * 1.3} cy={fullHeight * 0.9} r={80} />
<Orb cx={fullWidth * 0.72} cy={fullHeight * 0.58} r={17} />
<Orb cx={fullWidth * 0.78} cy={fullHeight * 0.78} r={40} />
<Orb cx={fullWidth * 1.25} cy={fullHeight * 0.66} r={30} />
<Orb cx={fullWidth * 1.05} cy={fullHeight * 0.56} r={12} />
<Orb cx={fullWidth * 1.5} cy={fullHeight * 0.54} r={11} />
<Orb cx={fullWidth * 1.8} cy={fullHeight * 0.8} r={66} />
<Orb cx={fullWidth * 1.8} cy={fullHeight * 0.6} r={27} />
</Group>
</Canvas>
</View>
);
}
38 changes: 12 additions & 26 deletions src/app/(app)/onboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,15 @@ import {
ColorMatrix,
Fill,
Group,
Image,
LinearGradient,
Paint,
Paragraph,
RoundedRect,
SkImage,
SkParagraphStyle,
SkTextStyle,
Skia,
TextAlign,
useFonts,
useImage,
vec,
} from "@shopify/react-native-skia";
import * as Notifications from "expo-notifications";
Expand All @@ -32,6 +29,7 @@ import { drawBigFont } from "@/src/common/utils";
import { useNotificationsPermissions } from "@/src/components/NotificationsStatus";
import { PrivacyPolicyLink } from "@/src/components/PrivacyPolicyLink";
import { SafeAreaView } from "@/src/components/SafeAreaView";
import { OnboardingScene } from "@/src/components/canvas/OnboardingScene";
import { fonts, palette, sharedStyles as ss } from "@/src/styles";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { router } from "expo-router";
Expand Down Expand Up @@ -71,27 +69,20 @@ export default function OnboardingScreen() {
{
// WELCOME
headerText: t("ONBOARDING_WELCOME_HEADER_I18N.string"),
image: useImage(require("@/assets/images/onboarding-welcome.png")),
bodyText: t("ONBOARDING_WELCOME_BODY_I18N.string"),
buttonText: t("ONBOARDING_WELCOME_BUTTON_I18N.string"),
beforeNext: undefined,
},
{
// INFO_1
headerText: t("ONBOARDING_INFO_1_HEADER_I18N.string"),
image: useImage(
require("@/assets/images/onboarding-instructions.png"),
),
bodyText: t("ONBOARDING_INFO_1_BODY_I18N.string"),
buttonText: t("ONBOARDING_INFO_1_BUTTON_I18N.string"),
beforeNext: undefined,
},
{
// PERMISSIONS
headerText: t("ONBOARDING_PERMISSIONS_HEADER_I18N.string"),
image: useImage(
require("@/assets/images/onboarding-permissions.png"),
),
bodyText: t("ONBOARDING_PERMISSIONS_BODY_I18N.string"),
buttonText: shouldAskForNotifications
? t("ONBOARDING_ENABLE_NOTIFICATIONS_BUTTON_I18N.string")
Expand All @@ -105,9 +96,6 @@ export default function OnboardingScreen() {
{
// PRIVACY POLICY
headerText: t("ONBOARDING_PRIVACY_POLICY_HEADER_I18N.string"),
image: useImage(
require("@/assets/images/onboarding-privacy-policy.png"),
),
bodyText: t("ONBOARDING_PRIVACY_POLICY_BODY_I18N.string"),
buttonText: t("ONBOARDING_PRIVACY_POLICY_BUTTON_I18N.string"),
beforeNext: undefined,
Expand Down Expand Up @@ -138,12 +126,12 @@ export default function OnboardingScreen() {
width: usableWidth * 0.96,
};
// image takes up the next 33% of usableHeight (48% total)
const imageTransform = [
const sceneTransform = [
{ translateY: usableHeight * 0.15 },
{ translateX: usableWidth * 0.18 },
//{ translateX: usableWidth * 0.18 },
];
const imageSize = {
width: usableWidth * 0.66,
const sceneSize = {
width: usableWidth,
height: usableHeight * 0.25,
};
// body takes up the next 31% of usableHeight (79% total)
Expand Down Expand Up @@ -171,6 +159,7 @@ export default function OnboardingScreen() {
height: usableHeight * 0.08,
};
const buttonBorderRadius = 15;
const privacyPolicyHeight = usableHeight * 0.05;
// 10% of usable height is left for the Privacy Policy link to appear in

const fontMgr = useFonts({
Expand Down Expand Up @@ -204,10 +193,6 @@ export default function OnboardingScreen() {
.build();
});

const image = useDerivedValue<SkImage | null>(() => {
return views[currentView.value].image;
});

const bodyP = useDerivedValue(() => {
if (!fontMgr) {
return null;
Expand Down Expand Up @@ -396,11 +381,11 @@ export default function OnboardingScreen() {
width={headerSize.width}
/>
</Group>
<Group transform={imageTransform}>
<Image
image={image}
width={imageSize.width}
height={imageSize.height}
<Group transform={sceneTransform}>
<OnboardingScene
currentView={currentView}
sceneWidth={sceneSize.width}
sceneHeight={sceneSize.height}
/>
</Group>
<Group transform={bodyTransform}>
Expand Down Expand Up @@ -511,6 +496,7 @@ export default function OnboardingScreen() {
<Animated.View style={{ opacity: privacyPolicyLinkOpacity }}>
<PrivacyPolicyLink
textStyle={{ ...ss.boldFont, ...ss.whiteText }}
containerHeight={privacyPolicyHeight}
/>
</Animated.View>
</Animated.View>
Expand Down
2 changes: 1 addition & 1 deletion src/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export default function Index() {
if (hasOnboarded !== null) {
runOnJS(router.replace)("/(app)/");
} else {
runOnJS(router.replace)("/(app)/onboarding");
runOnJS(router.replace)("/(app)/intro");
}
});
}
Expand Down
15 changes: 13 additions & 2 deletions src/components/ConduitOrbToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -425,8 +425,19 @@ export function ConduitOrbToggle({
}
canvasWidth={width}
orbRadius={finalOrbRadius}
orbCenterY={orbCenterY}
psiphonLogoSize={psiphonLogoSize}
midPoint={vec(0, 0)}
secondLastPoint={vec(
0,
-finalOrbRadius,
)}
endPoint={vec(
0,
-(
orbCenterY -
psiphonLogoSize / 2
),
)} // land in P logo
randomize={true}
/>
);
},
Expand Down
Loading

0 comments on commit d014fb5

Please sign in to comment.