Skip to content

Instantly share code, notes, and snippets.

@firstChairCoder
Last active August 12, 2022 15:57
Show Gist options
  • Save firstChairCoder/9a69b23602c037585fc3ad939912bd2e to your computer and use it in GitHub Desktop.
Save firstChairCoder/9a69b23602c037585fc3ad939912bd2e to your computer and use it in GitHub Desktop.
InnoTech Showcase 1-3
/* 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>
);
};
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.
// 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",
];
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>
);
};
/* 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>
);
};
@firstChairCoder
Copy link
Author

A gif showing its implementation:

innotech_showcas3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment