Last active
August 12, 2022 15:57
-
-
Save firstChairCoder/9a69b23602c037585fc3ad939912bd2e to your computer and use it in GitHub Desktop.
InnoTech Showcase 1-3
This file contains 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
/* eslint-disable react-native/no-inline-styles */ | |
/* eslint-disable no-shadow */ | |
import React, { useState, useMemo } from "react"; | |
import { StatusBar, Pressable, View, StyleSheet } from "react-native"; | |
import Constants from "expo-constants"; | |
import { Feather } from "@expo/vector-icons"; | |
import { MotiView, MotiText } from "moti"; | |
const _activeColor = "#80D15A"; | |
const _inactiveColor = "#BBC0D5"; | |
const _spacing = 20; | |
const _particlesCount = 8; | |
const size = 32; | |
const _innerSize = size * 0.2; | |
const styles = StyleSheet.create({ | |
container: { | |
flex: 1, | |
justifyContent: "center", | |
paddingTop: Constants.statusBarHeight, | |
backgroundColor: "#fff", | |
padding: _spacing, | |
}, | |
particles: { | |
width: _innerSize, | |
height: _innerSize, | |
borderRadius: _innerSize, | |
backgroundColor: _activeColor, | |
position: "absolute", | |
}, | |
text: { | |
fontSize: 24, | |
fontWeight: "bold", | |
lineHeight: (size / 4) * 3 + 1.5, | |
}, | |
tickWrapper: { | |
alignItems: "center", | |
justifyContent: "center", | |
width: size, | |
marginRight: _spacing, | |
borderRadius: size / 2, | |
borderWidth: 1, | |
borderColor: _inactiveColor, | |
}, | |
}); | |
const _todos = [ | |
"Drink Coffee", | |
"Break Bread", | |
"Write code", | |
"Take Laura to dinner", | |
"Procrastinate", | |
].map((item) => { | |
return { | |
key: item, | |
label: item, | |
checked: false, | |
}; | |
}); | |
const CheckBox = React.memo(({ checked, text, size }) => { | |
// const _innerSize = size * 0.2; | |
const [width, setWidth] = useState(size); | |
const particles = useMemo(() => { | |
return [...Array(_particlesCount).keys()].map((i) => { | |
const _angle = (i * 2 * Math.PI) / _particlesCount; | |
const _radius = 15; | |
return { | |
key: `particle-${i}`, | |
x: _radius * Math.cos(_angle), | |
y: _radius * Math.sin(_angle), | |
}; | |
}); | |
}, [size]); | |
return ( | |
<MotiView | |
style={{ flexDirection: "row" }} | |
from={{ opacity: 0 }} | |
animate={{ opacity: 1 }} | |
> | |
<View style={styles.tickWrapper}> | |
<MotiView | |
from={{ | |
scale: 0, | |
opacity: 0, | |
}} | |
animate={{ | |
scale: checked ? 1 : 0, | |
opacity: checked ? 1 : 0, | |
}} | |
transition={{ | |
type: checked ? "spring" : "timing", | |
duration: checked && 0, | |
}} | |
> | |
<Feather name="check" size={size} color={_activeColor} /> | |
</MotiView> | |
{width !== size && | |
particles.map((item) => { | |
return ( | |
<MotiView | |
key={item.key} | |
animate={{ | |
translateX: checked ? item.x : 0, | |
translateY: checked ? item.y : 0, | |
opacity: checked ? [0.5, 0] : 0, | |
scale: checked ? 1.2 : 1, | |
}} | |
transition={{ | |
type: "timing", | |
duration: 300, | |
delay: 100, | |
}} | |
style={styles.particles} | |
/> | |
); | |
})} | |
</View> | |
<MotiView | |
animate={{ | |
translateX: checked ? [24 / 2, 0] : 0, | |
}} | |
transition={{ | |
type: "timing", | |
duration: 200, | |
delay: 100, | |
}} | |
style={{ justifyContent: "center" }} | |
onLayout={(ev) => { | |
const newWidth = ev.nativeEvent.layout.width; | |
if (width !== newWidth) { | |
setWidth(newWidth); | |
// console.log(width); | |
} | |
}} | |
> | |
<MotiText | |
style={[ | |
styles.text, | |
{ color: checked ? _inactiveColor : _activeColor }, | |
]} | |
> | |
{text} | |
</MotiText> | |
{width !== size && ( | |
<MotiView | |
animate={{ | |
translateX: checked ? -_spacing / 2 : -size - _spacing + size / 4, | |
width: checked ? width + _spacing : size / 2, | |
backgroundColor: checked ? _inactiveColor : _activeColor, | |
}} | |
transition={{ | |
type: checked ? "spring" : "timing", | |
duration: checked && 0, | |
}} | |
style={{ | |
height: 3, | |
backgroundColor: _activeColor, | |
position: "absolute", | |
}} | |
/> | |
)} | |
</MotiView> | |
</MotiView> | |
); | |
}); | |
export const CheckNote = () => { | |
const [todos, setTodos] = useState(_todos); | |
return ( | |
<View style={styles.container}> | |
<StatusBar barStyle="dark-content" /> | |
{todos.map((todo) => { | |
return ( | |
//todo-item | |
<Pressable | |
key={todo.key} | |
style={{ marginBottom: _spacing }} | |
onPress={() => { | |
const { key } = todo; | |
const newTodos = todos.map((t) => { | |
if (t.key !== key) { | |
return t; | |
} | |
return { | |
...t, | |
checked: !t.checked, | |
}; | |
}); | |
setTodos(newTodos); | |
// console.log(newTodos); | |
}} | |
> | |
<CheckBox checked={todo.checked} text={todo.label} size={size} /> | |
</Pressable> | |
); | |
})} | |
</View> | |
); | |
}; |
This file contains 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
These are extracted screens from previous work done. | |
They are compacted into a single page rather than in separate React components, as per the Single Responsibility Principle only reduce the amount of breakages in reading the code. | |
Comments have been provided to guide through some parts of the code. | |
Principal libraries in use include: | |
-React Native Reanimated - Supercharged animations library for React Native | |
-React Native Moti - Set of helper modules for Reanimated v2. | |
-React Native Svg - Helps in drawing SVG path on screen. | |
-React Native Gesture Handler - | |
; among others. | |
This is written in JavaScript. For a sample repo written in TypeScript, please see the repo linked here: https://github.com/firstChairCoder/Rick-and-Morty-API-Coding-Challenge/tree/typed . It shows an interaction with GraphQL API. |
This file contains 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
// Grabbed from https://openpeeps.com/ | |
/* | |
copy(JSON.stringify([...document.querySelectorAll('.w-dyn-item .button-set > a')].map(x => x.href))) | |
*/ | |
export default [ | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e535d507371bb20aea29659_peep-100.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e535bba5197058548a68914_peep-86.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e535d897488c25a204b12ff_peep-102.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e535b5e8becbf0e0b545ab7_peep-83.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e53523e8e24936f0704284f_peep-17.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e5358e38e249322cc0675e2_peep-62.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e51c6f28c34f85f3c498170_peep-5.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e53540664109d6cbf005ad4_peep-27.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e53517fc6b2492d63287d6d_peep-11.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e535427f3aa4b96832be329_peep-28.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e535b9cd8713177a910e39b_peep-85.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e5354eec99250f2f5c64d48_peep-33.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e5359767371bb36089febe4_peep-67.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e5350c79b55b043e751037a_peep-5.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e535cfb4600807d898fc75b_peep-97.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e53539b550b7634d6f2aade_peep-25.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e53521c4600805ff88b3bb5_peep-16.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e535a4e8becbf15ac53ded2_peep-74.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e5350e2d87131d9ff0acac5_peep-6.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e532a5133d3686ff53d2a74_peep-2.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e535b238becbfb1a454450e_peep-81.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e535b7d460080f0198eabb9_peep-84.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e5358af8e24939dc40660de_peep-60.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e535c632b568a7abf1af4de_peep-92.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e5351f551970508e6a249c4_peep-15.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e535d195197053fe1a71f4b_peep-98.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e53585a550b766229f5afa9_peep-57.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e535c97550b761e38f7f4cd_peep-94.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e535cc0550b76297bf811bb_peep-95.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e51c7148c34f800d3498229_peep-6.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e51c6d88c34f86ae14980f1_peep-4.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e53595a7371bb55159fd9a2_peep-66.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e53566fc992503ea6c77af5_peep-41.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e53525c9588e0829a7b6d29_peep-18.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e535c2cf5fa1a249bfcafc9_peep-90.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e5351418e2493653d03a995_peep-9.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e535793d399238d5e54a0b5_peep-51.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e535902c9925085c4c9b4f8_peep-63.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e53566fc992503ea6c77af5_peep-41.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e535bec8e2493189608113e_peep-88.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e535bd3460080e5d68ef5ac_peep-87.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e5358c87371bb618f9f6b25_peep-61.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e5356eb9588e080d27e88e2_peep-45.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e535741f5fa1a13a1f8f233_peep-48.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e535a30d871312cf4100aed_peep-73.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e535840d39923925454c88d_peep-56.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e5356c67371bb2b069e35e3_peep-44.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e5353362b568a99fd167467_peep-21.png", | |
"https://assets.website-files.com/5e51c674258ffe10d286d30a/5e5353ecf3aa4bf2fc2bbf6b_peep-26.png", | |
]; |
This file contains 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, | |
StyleSheet, | |
FlatList, | |
Dimensions, | |
StatusBar, | |
} from "react-native"; | |
import faker from "faker"; | |
import Animated, { | |
Extrapolate, | |
interpolate, | |
useSharedValue, | |
useAnimatedStyle, | |
useAnimatedScrollHandler, | |
} from "react-native-reanimated"; | |
import { MotiView, useDynamicAnimation } from "moti"; | |
import Constants from "expo-constants"; | |
import data from "../data/peepsData"; | |
const DOT_SIZE = 8; | |
const { width, height } = Dimensions.get("window"); | |
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList); | |
const HEADINGS = [ | |
"Welcome \nto Peeps", | |
"A hand drawn \nillustration library", | |
"Explore various combinations", | |
"Designed by \nthe top illustrators \nand designers", | |
"Encapsulated by firstChairCoder", | |
"Come join us!", | |
]; | |
//These two functions define the radius and border of the circles. | |
const random = () => { | |
return ((Math.random() > 0.5 ? -1 : 1) * Math.random() * width) / 2; | |
}; | |
const randomBorder = () => { | |
return Math.floor(Math.random() * 14) + 4; | |
}; | |
const styles = StyleSheet.create({ | |
container: { | |
flex: 1, | |
justifyContent: "center", | |
backgroundColor: "#FFF", | |
paddingTop: Constants.statusBarHeight, | |
}, | |
circle: { | |
width: width * 0.8, | |
height: width * 0.8, | |
borderRadius: width, | |
borderWidth: 4, | |
borderColor: "rgba(0,0,0,1)", | |
position: "absolute", | |
}, | |
circlesWrapper: { | |
position: "absolute", | |
top: height * 0.1, | |
}, | |
itemImg: { | |
flex: 1, | |
resizeMode: "contain", | |
}, | |
textItem: { | |
position: "absolute", | |
}, | |
dotsWrapper: { | |
flexDirection: "row", | |
position: "absolute", | |
bottom: height * 0.1, | |
left: 20, | |
}, | |
dot: { | |
width: DOT_SIZE, | |
height: DOT_SIZE, | |
borderRadius: DOT_SIZE, | |
backgroundColor: "#000", | |
marginHorizontal: DOT_SIZE / 2, | |
}, | |
footer: { | |
position: "absolute", | |
bottom: height * 0.3, | |
left: 20, | |
width: width * 0.7, | |
}, | |
}); | |
// shuffle images function | |
faker.seed(123); | |
const _data = faker.helpers.shuffle(data).slice(0, HEADINGS.length); | |
const Circle = ({ animation }) => { | |
return ( | |
<MotiView | |
state={animation} | |
transition={{ stiffness: 50 }} | |
style={styles.circle} | |
/> | |
); | |
}; | |
const Circles = ({ first, second, third }) => { | |
return ( | |
<View style={styles.circlesWrapper}> | |
<Circle animation={first} /> | |
<Circle animation={second} /> | |
<Circle animation={third} /> | |
</View> | |
); | |
}; | |
const Item = ({ item, index, scrollX }) => { | |
const style = useAnimatedStyle(() => { | |
return { | |
opacity: interpolate( | |
scrollX.value / width, | |
[index - 0.6, index, index + 0.6], | |
[0, 1, 0] | |
), | |
}; | |
}); | |
return ( | |
<View style={{ width, height: height / 2 }}> | |
<Animated.Image style={[styles.itemImg, style]} source={{ uri: item }} /> | |
</View> | |
); | |
}; | |
const TextItem = ({ index, heading, scrollX }) => { | |
const style = useAnimatedStyle(() => { | |
return { | |
opacity: interpolate( | |
scrollX.value / width, | |
[index - 0.8, index, index + 0.8], | |
[0, 1, 0] | |
), | |
transform: [ | |
{ | |
translateX: interpolate( | |
scrollX.value / width, | |
[index - 0.8, index, index + 0.8], | |
[10, 0, -10] | |
), | |
}, | |
], | |
}; | |
}); | |
return ( | |
<Animated.Text | |
key={index} | |
// eslint-disable-next-line react-native/no-inline-styles | |
style={[styles.textItem, { fontSize: index === 0 ? 42 : 28 }, style]} | |
> | |
{heading} | |
</Animated.Text> | |
); | |
}; | |
//control Dot size based on active slide/heading. | |
const PaginationDot = ({ index, scrollX }) => { | |
const style = useAnimatedStyle(() => { | |
return { | |
width: interpolate( | |
scrollX.value / width, | |
[index - 1, index, index + 1], | |
[DOT_SIZE * 1.5, DOT_SIZE * 3, DOT_SIZE * 1.5], | |
Extrapolate.CLAMP | |
), | |
opacity: interpolate( | |
scrollX.value / width, | |
[index - 1, index, index + 1], | |
[0.2, 1, 0.2], | |
Extrapolate.CLAMP | |
), | |
}; | |
}); | |
return <Animated.View style={[styles.dot, style]} />; | |
}; | |
// eslint-disable-next-line no-shadow | |
const Pagination = ({ data, scrollX }) => { | |
return ( | |
<View style={styles.dotsWrapper}> | |
{data.map((_, index) => ( | |
<PaginationDot index={index} scrollX={scrollX} /> | |
))} | |
</View> | |
); | |
}; | |
export const PeepsScreen = () => { | |
const scrollX = useSharedValue(0); | |
const onScroll = useAnimatedScrollHandler((ev) => { | |
scrollX.value = ev.contentOffset.x; | |
}); | |
const first = useDynamicAnimation(() => ({ | |
translateX: random(), | |
translateY: random(), | |
width: width * 0.8, | |
height: width * 0.8, | |
borderRadius: width * 0.8, | |
borderWidth: randomBorder(), | |
})); | |
const second = useDynamicAnimation(() => ({ | |
translateX: random(), | |
translateY: random(), | |
width: width * 0.8, | |
height: width * 0.8, | |
borderRadius: width * 0.8, | |
borderWidth: randomBorder(), | |
})); | |
const third = useDynamicAnimation(() => ({ | |
translateX: random(), | |
translateY: random(), | |
width: width * 0.8, | |
height: width * 0.8, | |
borderRadius: width * 0.8, | |
borderWidth: randomBorder(), | |
})); | |
return ( | |
<View style={styles.container}> | |
<StatusBar hidden /> | |
<Circles first={first} second={second} third={third} /> | |
<AnimatedFlatList | |
data={_data} | |
keyExtractor={(item) => item} | |
renderItem={({ item, index }) => { | |
return <Item item={item} index={index} scrollX={scrollX} />; | |
}} | |
horizontal | |
pagingEnabled | |
showsHorizontalScrollIndicator={false} | |
onScroll={onScroll} | |
scrollEventThrotle={16} | |
bounces={false} | |
onMomentumScrollEnd={(e) => { | |
const newSize = width * 0.5 + Math.random() * width * 0.5; | |
const newSize2 = width * 0.5 + Math.random() * width * 0.5; | |
const newSize3 = width * 0.5 + Math.random() * width * 0.5; | |
first.animateTo({ | |
translateX: random(), | |
translateY: random(), | |
width: newSize, | |
height: newSize, | |
borderRadius: newSize, | |
borderWidth: randomBorder(), | |
}); | |
second.animateTo({ | |
translateX: random(), | |
translateY: random(), | |
width: newSize2, | |
height: newSize2, | |
borderRadius: newSize2, | |
borderWidth: randomBorder(), | |
}); | |
third.animateTo({ | |
translateX: random(), | |
translateY: random(), | |
width: newSize3, | |
height: newSize3, | |
borderRadius: newSize3, | |
borderWidth: randomBorder(), | |
}); | |
}} | |
/> | |
<View style={styles.footer}> | |
{HEADINGS.map((heading, index) => ( | |
<TextItem | |
key={index} | |
index={index} | |
heading={heading} | |
scrollX={scrollX} | |
/> | |
))} | |
</View> | |
<Pagination data={_data} scrollX={scrollX} /> | |
</View> | |
); | |
}; |
This file contains 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
/* eslint-disable react-native/no-inline-styles */ | |
import React from "react"; | |
import { | |
Dimensions, | |
StatusBar, | |
StyleSheet, | |
Text, | |
TextInput, | |
View, | |
} from "react-native"; | |
import { | |
GestureHandlerRootView, | |
PanGestureHandler, | |
} from "react-native-gesture-handler"; | |
import Animated, { | |
interpolate, | |
useAnimatedGestureHandler, | |
useAnimatedProps, | |
useAnimatedStyle, | |
useDerivedValue, | |
useSharedValue, | |
withSpring, | |
} from "react-native-reanimated"; | |
import Svg, { Path } from "react-native-svg"; | |
const { width, height } = Dimensions.get("window"); | |
const color = "#4985E0"; | |
//next two variables control minimum and maximum font size on pull gesture. | |
const minF = width * 0.1; | |
const maxF = width * 0.34; | |
//value that keeps this from covering the whole screen. also works with our minF and maxF functions below. | |
const clampMin = 25; | |
const clampMax = 75; | |
const styles = StyleSheet.create({ | |
animatedWrapper: { | |
position: "absolute", | |
backgroundColor: "transparent", | |
}, | |
bottomText: { | |
color: "#FFF", | |
fontSize: 24, | |
marginLeft: 20, | |
textAlign: "center", | |
}, | |
animatedWrapperTop: { | |
position: "absolute", | |
left: 0, | |
right: 0, | |
bottom: 0, | |
justifyContent: "flex-end", | |
paddingBottom: height * 0.1, | |
}, | |
animatedWrapperBottom: { | |
position: "absolute", | |
left: 0, | |
right: 0, | |
bottom: 0, | |
justifyContent: "flex-start", | |
paddingTop: height * 0.1, | |
}, | |
container: { | |
flex: 1, | |
backgroundColor: "#FFF", | |
}, | |
topText: { | |
color: color, | |
fontSize: 24, | |
marginLeft: 20, | |
textAlign: "center", | |
}, | |
}); | |
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput); | |
const AnimatedPath = Animated.createAnimatedComponent(Path); | |
//allows smooth animation of text when calculation causes bouncing. | |
Animated.addWhitelistedNativeProps({ text: true }); | |
const clamp = (value, min, max) => { | |
"worklet"; | |
return Math.min(Math.max(min, value), max); | |
}; | |
const AnimatedText = ({ text, style, ...props }) => { | |
const animatedProps = useAnimatedProps(() => { | |
if (!text) { | |
return {}; | |
} | |
return { | |
text: String(text.value), | |
}; | |
}); | |
return ( | |
<AnimatedTextInput | |
style={[style]} | |
editable={false} | |
allowFontScaling={true} | |
numberOfLines={1} | |
value={String(text.value)} | |
underlineColorAndroid={"transparent"} | |
{...{ animatedProps }} | |
/> | |
); | |
}; | |
export const ProgressBar = () => { | |
const posY = useSharedValue(50); | |
const currentY = useSharedValue(50); | |
const currentX = useSharedValue(width / 1.5); | |
const animatedProps = useAnimatedProps(() => { | |
const h = (height * posY.value) / 100; | |
const currentH = (height * currentY.value) / 100; | |
return { | |
// extend beyond the screenwidth to make this feel more natural. | |
d: ` | |
M-100 ${h} | |
C ${currentX.value} ${currentH}, ${currentX.value} ${currentH}, ${ | |
width + 100 | |
} ${h} | |
L${width + 100} ${height} | |
L0 ${height} | |
Z | |
`, | |
}; | |
}); | |
const gestureHandler = useAnimatedGestureHandler({ | |
onStart: (event, ctx) => { | |
ctx.startY = posY.value; | |
ctx.startX = event.x; | |
}, | |
onActive: (event, ctx) => { | |
posY.value = clamp( | |
ctx.startY + event.translationY / 50, | |
clampMin, | |
clampMax | |
); | |
currentY.value = ctx.startY + event.translationY / 18; | |
currentX.value = ctx.startX + event.translationX / 3; | |
}, | |
onEnd: (event, ctx) => { | |
currentY.value = withSpring(posY.value, { | |
damping: 3, | |
stiffness: 400, | |
}); | |
currentX.value = withSpring(width / 2, { | |
damping: 10, | |
stiffness: 100, | |
}); | |
}, | |
}); | |
const topViewStyle = useAnimatedStyle(() => { | |
return { | |
top: 0, | |
height: (height * posY.value) / 100, | |
transform: [ | |
{ | |
translateY: currentY.value / 2, | |
}, | |
], | |
}; | |
}); | |
const bottomViewStyle = useAnimatedStyle(() => { | |
return { | |
bottom: 0, | |
height: height - (height * posY.value) / 100, | |
transform: [ | |
{ | |
translateY: -currentY.value / 2, | |
}, | |
], | |
}; | |
}); | |
const topValue = useDerivedValue(() => { | |
return Math.floor(interpolate(posY.value, [clampMin, clampMax], [0, 100])); | |
}); | |
const bottomValue = useDerivedValue(() => { | |
return Math.ceil(interpolate(posY.value, [clampMin, clampMax], [100, 0])); | |
}); | |
const topTextStyle = useAnimatedStyle(() => { | |
return { | |
fontSize: Math.floor( | |
interpolate(posY.value, [clampMin, clampMax], [minF, maxF]) | |
), | |
}; | |
}); | |
const bottomTextStyle = useAnimatedStyle(() => { | |
return { | |
fontSize: Math.floor( | |
interpolate(posY.value, [clampMin, clampMax], [maxF, minF]) | |
), | |
}; | |
}); | |
return ( | |
<View style={styles.container}> | |
<StatusBar barStyle="dark-content" /> | |
<GestureHandlerRootView style={{ flex: 1 }}> | |
<PanGestureHandler onGestureEvent={gestureHandler}> | |
<Animated.View style={styles.animatedWrapper}> | |
<Svg width={width} height={height}> | |
<AnimatedPath fill={color} animatedProps={animatedProps} /> | |
</Svg> | |
<Animated.View style={[styles.animatedWrapperTop, topViewStyle]}> | |
<View style={{ flexDirection: "row", alignItems: "center" }}> | |
<AnimatedText | |
text={topValue} | |
style={[ | |
topTextStyle, | |
{ fontSize: minF + maxF / 2, marginLeft: 20, color: color }, | |
]} | |
/> | |
<Text style={styles.topText}>Points you {"\n"} need</Text> | |
</View> | |
</Animated.View> | |
<Animated.View | |
style={[styles.animatedWrapperBottom, bottomViewStyle]} | |
> | |
<View style={{ flexDirection: "row", alignItems: "center" }}> | |
<AnimatedText | |
text={bottomValue} | |
style={[ | |
{ | |
fontSize: minF + maxF / 2, | |
marginLeft: 20, | |
color: "#FFF", | |
}, | |
bottomTextStyle, | |
]} | |
/> | |
<Text style={styles.bottomText}>Points you {"\n"} have</Text> | |
</View> | |
</Animated.View> | |
</Animated.View> | |
</PanGestureHandler> | |
</GestureHandlerRootView> | |
</View> | |
); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
A gif showing its implementation: