Skip to content

Instantly share code, notes, and snippets.

@tanner-west
Created October 23, 2023 12:27
Show Gist options
  • Save tanner-west/e9b2fbb08965055ed9a1818d18af9129 to your computer and use it in GitHub Desktop.
Save tanner-west/e9b2fbb08965055ed9a1818d18af9129 to your computer and use it in GitHub Desktop.
Overworld
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