Created
October 23, 2023 12:27
-
-
Save tanner-west/e9b2fbb08965055ed9a1818d18af9129 to your computer and use it in GitHub Desktop.
Overworld
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from "react"; | |
import { View, useWindowDimensions, Image } from "react-native"; | |
import Svg, { Line } from "react-native-svg"; | |
import Animated, { | |
Extrapolation, | |
SharedValue, | |
interpolate, | |
useAnimatedRef, | |
useAnimatedStyle, | |
useScrollViewOffset, | |
} from "react-native-reanimated"; | |
import { | |
Canvas, | |
Rect, | |
vec, | |
Circle as SkCircle, | |
LinearGradient, | |
} from "@shopify/react-native-skia"; | |
const lineColor = "#888"; | |
export const SkyGradient = () => { | |
const windowDimensions = useWindowDimensions(); | |
return ( | |
<Canvas style={{ flex: 1 }}> | |
{/* Sky */} | |
<Rect | |
x={0} | |
y={0} | |
width={windowDimensions.width} | |
height={windowDimensions.height / 2} | |
> | |
<LinearGradient | |
start={vec(0, windowDimensions.width)} | |
end={vec(0, windowDimensions.height / 20)} | |
colors={["#FF48C4", "#ff3f3f"]} | |
/> | |
</Rect> | |
{/* Sun */} | |
<SkCircle | |
r={100} | |
cx={windowDimensions.width / 2} | |
cy={windowDimensions.height / 2 - 10} | |
> | |
<LinearGradient | |
start={vec( | |
windowDimensions.width / 2, | |
windowDimensions.height / 2 - 80 | |
)} | |
end={vec(windowDimensions.width / 2, windowDimensions.height / 2)} | |
colors={["#FDF9CE", "#DC8300"]} | |
/> | |
</SkCircle> | |
</Canvas> | |
); | |
}; | |
const OverworldScreen = () => { | |
const windowDimensions = useWindowDimensions(); | |
const svgHeight = windowDimensions.height * 4; | |
const lineSpacing = svgHeight / 100; | |
const aref = useAnimatedRef<Animated.ScrollView>(); | |
const scrollHandler = useScrollViewOffset(aref); | |
// This view renders the horizontal lines in the scrolling grid | |
const HorizontalLineView = ({ | |
index, | |
offset, | |
}: { | |
index: number; | |
offset: any; | |
}) => { | |
const lineAnimatedStyles = useAnimatedStyle(() => { | |
const scrollIndex = offset.value / lineSpacing; | |
// Interpolate the height so it gets smaller as it approaches the horizon, thus making the lines appear closer together | |
const height = interpolate( | |
scrollIndex, | |
[index - 10, index, index + 1], | |
[lineSpacing, lineSpacing * 0.2, lineSpacing], | |
Extrapolation.CLAMP | |
); | |
// Make the lines disappear as they cross the horizon. I do this so the other scrollview children (the buildings) can remain visible after they cross | |
const opacity = interpolate( | |
scrollIndex, | |
[index, index + 1], | |
[1, 0], | |
Extrapolation.CLAMP | |
); | |
return { | |
height, | |
opacity, | |
}; | |
}); | |
return ( | |
<Animated.View | |
style={[ | |
{ | |
borderBottomWidth: 1, | |
borderColor: lineColor, | |
height: lineSpacing, | |
width: windowDimensions.width, | |
}, | |
lineAnimatedStyles, | |
]} | |
/> | |
); | |
}; | |
const Lines = ({ scrollOffset }: { scrollOffset: SharedValue<number> }) => { | |
return Array.from({ length: 100 }).map((_, i) => ( | |
<HorizontalLineView index={i} key={i} offset={scrollOffset} /> | |
)); | |
}; | |
const Buildings = ({ | |
scrollOffset, | |
}: { | |
scrollOffset: SharedValue<number>; | |
}) => { | |
return [ | |
{ | |
topOffset: 100, | |
leftOffset: 50, | |
width: 100, | |
imgSrc: require("../assets/images/buildings/building_1.png"), | |
}, | |
{ | |
topOffset: svgHeight * 0.05, | |
leftOffset: 225, | |
width: 100, | |
imgSrc: require("../assets/images/buildings/building_2.png"), | |
}, | |
{ | |
topOffset: svgHeight * 0.1, | |
leftOffset: 100, | |
width: 100, | |
imgSrc: require("../assets/images/buildings/building_3.png"), | |
}, | |
{ | |
topOffset: svgHeight * 0.12, | |
leftOffset: windowDimensions.width * 0.8, | |
width: 100, | |
imgSrc: require("../assets/images/buildings/building_24.png"), | |
}, | |
{ | |
topOffset: svgHeight * 0.15, | |
leftOffset: 20, | |
width: 100, | |
imgSrc: require("../assets/images/buildings/building_4.png"), | |
}, | |
{ | |
topOffset: svgHeight * 0.2, | |
leftOffset: 250, | |
width: 100, | |
imgSrc: require("../assets/images/buildings/building_5.png"), | |
}, | |
{ | |
topOffset: svgHeight * 0.22, | |
leftOffset: 10, | |
width: 100, | |
imgSrc: require("../assets/images/buildings/building_23.png"), | |
}, | |
{ | |
topOffset: svgHeight * 0.25, | |
leftOffset: 200, | |
width: 100, | |
imgSrc: require("../assets/images/buildings/building_12.png"), | |
}, | |
{ | |
topOffset: svgHeight * 0.3, | |
leftOffset: 0, | |
width: 100, | |
imgSrc: require("../assets/images/buildings/building_14.png"), | |
}, | |
{ | |
topOffset: svgHeight * 0.33, | |
leftOffset: 500, | |
width: 100, | |
imgSrc: require("../assets/images/buildings/building_11.png"), | |
}, | |
{ | |
topOffset: svgHeight * 0.38, | |
leftOffset: 100, | |
width: 100, | |
imgSrc: require("../assets/images/buildings/building_20.png"), | |
}, | |
{ | |
topOffset: svgHeight * 0.4, | |
leftOffset: 300, | |
width: 100, | |
imgSrc: require("../assets/images/buildings/building_15.png"), | |
}, | |
{ | |
topOffset: svgHeight * 0.45, | |
leftOffset: 200, | |
width: 100, | |
imgSrc: require("../assets/images/buildings/building_21.png"), | |
}, | |
{ | |
topOffset: svgHeight * 0.5, | |
leftOffset: 400, | |
width: 100, | |
imgSrc: require("../assets/images/buildings/building_13.png"), | |
}, | |
{ | |
topOffset: svgHeight * 0.55, | |
leftOffset: 20, | |
width: 100, | |
imgSrc: require("../assets/images/buildings/building_16.png"), | |
}, | |
{ | |
topOffset: svgHeight * 0.6, | |
leftOffset: 80, | |
width: 100, | |
imgSrc: require("../assets/images/buildings/building_6.png"), | |
}, | |
{ | |
topOffset: svgHeight * 0.66, | |
leftOffset: 20, | |
width: 100, | |
imgSrc: require("../assets/images/buildings/building_17.png"), | |
}, | |
{ | |
topOffset: svgHeight * 0.7, | |
leftOffset: 180, | |
width: 100, | |
imgSrc: require("../assets/images/buildings/building_7.png"), | |
}, | |
{ | |
topOffset: svgHeight * 0.72, | |
leftOffset: 400, | |
width: 100, | |
imgSrc: require("../assets/images/buildings/building_18.png"), | |
}, | |
{ | |
topOffset: svgHeight * 0.75, | |
leftOffset: 0, | |
width: 100, | |
imgSrc: require("../assets/images/buildings/building_8.png"), | |
}, | |
{ | |
topOffset: svgHeight * 0.8, | |
leftOffset: 400, | |
width: 100, | |
imgSrc: require("../assets/images/buildings/building_9.png"), | |
}, | |
{ | |
topOffset: svgHeight * 0.85, | |
leftOffset: 200, | |
width: 100, | |
imgSrc: require("../assets/images/buildings/building_19.png"), | |
}, | |
{ | |
topOffset: svgHeight * 0.9, | |
leftOffset: 300, | |
width: 100, | |
imgSrc: require("../assets/images/buildings/building_10.png"), | |
}, | |
{ | |
topOffset: svgHeight * 0.92, | |
leftOffset: 2, | |
width: 100, | |
imgSrc: require("../assets/images/buildings/building_25.png"), | |
}, | |
{ | |
topOffset: svgHeight - 100, | |
leftOffset: windowDimensions.width / 2, | |
width: 100, | |
imgSrc: require("../assets/images/buildings/building_31.png"), | |
}, | |
].map((bld) => ( | |
<AnimatedBuilding | |
key={bld.imgSrc} | |
topOffset={bld.topOffset} | |
leftOffset={bld.leftOffset} | |
width={bld.width} | |
scrollOffset={scrollOffset} | |
imgSrc={bld.imgSrc} | |
/> | |
)); | |
}; | |
const AnimatedBuilding = ({ | |
topOffset, | |
leftOffset, | |
width, | |
scrollOffset, | |
imgSrc, | |
}: { | |
topOffset: number; | |
leftOffset: number; | |
width: number; | |
scrollOffset: SharedValue<number>; | |
imgSrc: number; | |
}) => { | |
const animatedBuildingStyle = useAnimatedStyle(() => { | |
const halfScreenWidth = windowDimensions.width / 2; | |
const distanceFromCenter = halfScreenWidth - leftOffset - 50; | |
// Finding the right output ranges for these values is by no means a science. It just took a lot of trial and error to find something that looked realistic. There's a lot of room for improvement. | |
const scale = interpolate( | |
scrollOffset.value, | |
[topOffset - windowDimensions.height / 2 - 50, topOffset + 25], | |
[3, 0], | |
Extrapolation.CLAMP | |
); | |
const translateX = interpolate( | |
scrollOffset.value, | |
[topOffset - windowDimensions.height / 2 - 50, topOffset + 25], | |
[0, (distanceFromCenter * 0.8) / (scale || 1)], | |
Extrapolation.CLAMP | |
); | |
const translateY = interpolate( | |
scrollOffset.value, | |
[topOffset - windowDimensions.height / 2 - 50, topOffset + 25], | |
[0, -150], | |
Extrapolation.CLAMP | |
); | |
const opacity = interpolate( | |
scrollOffset.value, | |
[topOffset - 120, topOffset - 60], | |
[1, 0], | |
Extrapolation.CLAMP | |
); | |
return { | |
opacity, | |
transform: [{ scale }, { translateX }, { translateY }], | |
}; | |
}); | |
return ( | |
<Animated.View | |
collapsable={false} | |
style={[ | |
{ | |
position: "absolute", | |
top: topOffset - width / 2, | |
left: leftOffset, | |
zIndex: 100, | |
}, | |
animatedBuildingStyle, | |
]} | |
> | |
<Image | |
style={{ height: 100, width: 100 }} | |
source={imgSrc} | |
resizeMode="contain" | |
/> | |
</Animated.View> | |
); | |
}; | |
return ( | |
<View style={{ flex: 1, backgroundColor: "#111" }}> | |
<View | |
style={{ | |
flex: 1, | |
justifyContent: "flex-end", | |
flexDirection: "column", | |
}} | |
> | |
<SkyGradient /> | |
</View> | |
<View | |
style={{ | |
height: windowDimensions.height / 2, | |
borderTopWidth: 1, | |
borderColor: lineColor, | |
}} | |
> | |
{/* This SVG renders the vertical lines of the scrolling grid */} | |
<Svg | |
height={svgHeight} | |
width={windowDimensions.width} | |
style={{ position: "absolute" }} | |
> | |
{/* Center */} | |
<Line | |
x1={windowDimensions.width / 2} | |
y1={0} | |
x2={windowDimensions.width / 2} | |
y2={windowDimensions.width} | |
stroke={lineColor} | |
strokeWidth="1" | |
/> | |
{/* 1st Left */} | |
<Line | |
x1={windowDimensions.width * 0.4} | |
y1={0} | |
x2={windowDimensions.width * 0.2} | |
y2={windowDimensions.width} | |
stroke={lineColor} | |
strokeWidth="1" | |
/> | |
{/* 2nd Left */} | |
<Line | |
x1={windowDimensions.width * 0.3} | |
y1={0} | |
x2={0} | |
y2={windowDimensions.width * 0.8} | |
stroke={lineColor} | |
strokeWidth="1" | |
/> | |
{/* 3rd Left */} | |
<Line | |
x1={windowDimensions.width * 0.2} | |
y1={0} | |
x2={0} | |
y2={windowDimensions.width * 0.4} | |
stroke={lineColor} | |
strokeWidth="1" | |
/> | |
{/* 4th Left */} | |
<Line | |
x1={windowDimensions.width * 0.1} | |
y1={0} | |
x2={0} | |
y2={windowDimensions.width * 0.15} | |
stroke={lineColor} | |
strokeWidth="1" | |
/> | |
{/* 1st Right */} | |
<Line | |
x1={windowDimensions.width * 0.6} | |
y1={0} | |
x2={windowDimensions.width * 0.8} | |
y2={windowDimensions.width} | |
stroke={lineColor} | |
strokeWidth="1" | |
/> | |
{/* 2nd Right */} | |
<Line | |
x1={windowDimensions.width * 0.7} | |
y1={0} | |
x2={windowDimensions.width} | |
y2={windowDimensions.width * 0.8} | |
stroke={lineColor} | |
strokeWidth="1" | |
/> | |
{/* 3rd Right */} | |
<Line | |
x1={windowDimensions.width * 0.8} | |
y1={0} | |
x2={windowDimensions.width} | |
y2={windowDimensions.width * 0.4} | |
stroke={lineColor} | |
strokeWidth="1" | |
/> | |
{/* 4th Right */} | |
<Line | |
x1={windowDimensions.width * 0.9} | |
y1={0} | |
x2={windowDimensions.width} | |
y2={windowDimensions.width * 0.15} | |
stroke={lineColor} | |
strokeWidth="1" | |
/> | |
</Svg> | |
<Animated.ScrollView | |
style={{ | |
height: windowDimensions.width, | |
backgroundColor: "rgba(100, 0,100, 0.2)", | |
}} | |
ref={aref} | |
scrollEventThrottle={16} | |
overflow="visible" | |
snapToInterval={20} | |
simultaneous={"d"} | |
> | |
<Svg height={svgHeight} width={windowDimensions.width}> | |
<Lines scrollOffset={scrollHandler} /> | |
<Buildings scrollOffset={scrollHandler} /> | |
<View style={{ height: windowDimensions.height * 0.3 }}></View> | |
</Svg> | |
</Animated.ScrollView> | |
</View> | |
</View> | |
); | |
}; | |
export default OverworldScreen; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment