Skip to content

Instantly share code, notes, and snippets.

@tanner-west
Created October 30, 2023 12:24
Show Gist options
  • Save tanner-west/e7294263afddc7e5de1adc3072a00c7b to your computer and use it in GitHub Desktop.
Save tanner-west/e7294263afddc7e5de1adc3072a00c7b to your computer and use it in GitHub Desktop.
Boo!
import React from "react";
import Svg, {
Path,
Circle,
Defs,
ClipPath,
LinearGradient,
Stop,
Rect,
G,
RadialGradient,
} from "react-native-svg";
import { View, useWindowDimensions } from "react-native";
import Animated, {
Extrapolate,
interpolate,
useAnimatedProps,
useSharedValue,
} from "react-native-reanimated";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
const GhostScreen = () => {
const black = "#111";
const screenWidth = useWindowDimensions().width;
const screenHeight = useWindowDimensions().height;
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
const AnimatedPath = Animated.createAnimatedComponent(Path);
const panX = useSharedValue(0);
const panY = useSharedValue(0);
const panGesture = Gesture.Pan().onChange((e) => {
panX.value = e.absoluteX;
panY.value = e.absoluteY;
});
const animatedGhostPathProps = useAnimatedProps(() => {
const x = interpolate(panX.value, [0, screenWidth], [0, screenWidth]);
const y = interpolate(panY.value, [0, screenHeight], [-100, 200]);
return {
d: `M 100, 300
C ${x - 120}, ${y - 40}
${x + 120}, ${y - 40}
300, 300
C 290, 315
270, 315
260, 300
C 250, 285
230, 285
220, 300
C 210, 315
190, 315
180, 300
C 170, 285
150, 285
140, 300
C 130, 315
110, 315
100, 300`,
};
});
const animatedEyeLeftProps = useAnimatedProps(() => {
const x = interpolate(
panX.value,
[0, screenWidth],
[45, 295],
Extrapolate.CLAMP
);
const y = interpolate(
panY.value,
[0, screenHeight],
[0, 200],
Extrapolate.CLAMP
);
return {
cx: x,
cy: y + 75,
};
});
const animatedPupilLeftProps = useAnimatedProps(() => {
const x = interpolate(
panX.value,
[0, screenWidth],
[30, 310],
Extrapolate.CLAMP
);
const y = interpolate(
panY.value,
[0, screenHeight],
[-10, 210],
Extrapolate.CLAMP
);
return {
cx: x,
cy: y + 75,
};
});
const animatedEyeRightProps = useAnimatedProps(() => {
const x = interpolate(
panX.value,
[0, screenWidth],
[80, 330],
Extrapolate.CLAMP
);
const y = interpolate(
panY.value,
[0, screenHeight],
[0, 200],
Extrapolate.CLAMP
);
return {
cx: x,
cy: y + 75,
};
});
const animatedPupilRightProps = useAnimatedProps(() => {
const x = interpolate(
panX.value,
[0, screenWidth],
[65, 345],
Extrapolate.CLAMP
);
const y = interpolate(
panY.value,
[0, screenHeight],
[-10, 210],
Extrapolate.CLAMP
);
return {
cx: x,
cy: y + 75,
};
});
return (
<GestureDetector gesture={panGesture}>
<View style={{ flex: 1, justifyContent: "center" }}>
{/* Bottom half of screen */}
<Svg
width={screenWidth}
height={screenHeight}
style={{
position: "absolute",
backgroundColor: black,
top: 400,
left: 0,
}}
></Svg>
{/* Top half of screen */}
<Svg
width={screenWidth}
height={screenWidth}
style={{
position: "absolute",
top: 0,
left: 0,
}}
>
<Defs>
<LinearGradient id="grad" x1="1" y1="1" x2="1" y2="0">
<Stop offset="0" stopColor="#990099" stopOpacity="1" />
<Stop offset="40%" stopColor="#220022" stopOpacity="0.95" />
</LinearGradient>
<RadialGradient
id="moonGradient"
cx="300"
cy="150"
rx="75"
ry="75"
fx="300"
fy="150"
gradientUnits="userSpaceOnUse"
>
<Stop offset="0" stopColor="white" stopOpacity="1" />
<Stop offset="1" stopColor="white" stopOpacity="0" />
</RadialGradient>
</Defs>
<Rect
x={0}
y={0}
width={screenWidth}
height={400}
fill="url(#grad)"
/>
{/* moon */}
<Circle cx={300} cy={150} r={70} fill={"url(#moonGradient)"} />
<Circle cx={300} cy={150} r={40} fill={"#DDD"} />
</Svg>
{/* Ghost and graveyard */}
<Svg
width={screenWidth}
height={screenHeight}
style={{ zIndex: 1, overflow: "visible" }}
>
<Defs>
<ClipPath id={"circlePath"}>
<AnimatedCircle
animatedProps={animatedEyeLeftProps}
r={15}
fill={"blue"}
/>
</ClipPath>
<ClipPath id={"circlePath2"}>
<AnimatedCircle
animatedProps={animatedEyeRightProps}
r={15}
fill={"blue"}
/>
</ClipPath>
</Defs>
<G translateY={250}>
{/* Grave stones */}
<Rect
rotation={-10}
x={20}
y={120}
width={50}
height={50}
fill={black}
></Rect>
<Circle cx={65} cy={110} r={25} fill={black} />
<Rect x={300} y={120} width={50} height={50} fill={black}></Rect>
<Circle cx={325} cy={120} r={25} fill={black} />
<Rect x={125} y={100} width={50} height={75} fill={black}></Rect>
<Rect
rotation={5}
x={225}
y={75}
width={15}
height={75}
fill={black}
></Rect>
<Rect
rotation={5}
x={208}
y={90}
width={50}
height={15}
fill={black}
></Rect>
{/* Ghost */}
<AnimatedPath
fill="white"
stroke="white"
strokeWidth="2"
animatedProps={animatedGhostPathProps}
/>
<AnimatedCircle
animatedProps={animatedEyeLeftProps}
r={15}
fill={"lightblue"}
/>
<AnimatedCircle
animatedProps={animatedEyeRightProps}
r={15}
fill={"lightblue"}
/>
<AnimatedCircle
animatedProps={animatedPupilLeftProps}
r={10}
fill={black}
clipPath={"url(#circlePath)"}
/>
<AnimatedCircle
animatedProps={animatedPupilRightProps}
r={10}
fill={black}
clipPath={"url(#circlePath2)"}
/>
</G>
</Svg>
</View>
</GestureDetector>
);
};
export default GhostScreen;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment