Skip to content

Instantly share code, notes, and snippets.

@terrysahaidak
Last active April 18, 2022 19:08
Show Gist options
  • Save terrysahaidak/404d450c95a99318d8c1b9ed0fd6cd96 to your computer and use it in GitHub Desktop.
Save terrysahaidak/404d450c95a99318d8c1b9ed0fd6cd96 to your computer and use it in GitHub Desktop.
import {
Canvas,
LinearGradient,
Path,
useDerivedValue,
useValue,
vec,
Skia,
SkPath,
runTiming,
} from '@shopify/react-native-skia';
import React, {useEffect, useLayoutEffect, useRef, useState} from 'react';
import {
StyleSheet,
Text,
View,
Dimensions,
TouchableOpacity,
} from 'react-native';
import Animated, {
withTiming,
useAnimatedProps,
useSharedValue,
runOnUI,
} from 'react-native-reanimated';
import {mixPath, parse} from 'react-native-redash';
import Svg, {Path as SvgPath} from 'react-native-svg';
const window = Dimensions.get('window');
const AnimatedPath = Animated.createAnimatedComponent(SvgPath);
export type GraphProps = {
height?: number;
width?: number;
currentPath: SkPath;
previousPath: SkPath;
};
export const createGraphPath = (
width: number,
height: number,
steps: number,
round = true,
) => {
const retVal = Skia.Path.Make();
let y = height / 2;
retVal.moveTo(0, y);
const prevPt = {x: 0, y};
for (let i = 0; i < width; i += width / steps) {
// increase y by a random amount between -10 and 10
y += Math.random() * 30 - 15;
y = Math.max(height * 0.2, Math.min(y, height * 0.7));
if (round && i > 0) {
const xMid = (prevPt.x + i) / 2;
const yMid = (prevPt!.y + y) / 2;
retVal.quadTo(prevPt.x, prevPt.y, xMid, yMid);
prevPt.x = i;
prevPt.y = y;
} else {
retVal.lineTo(i, y);
}
}
return retVal;
};
const data = [
{
title: 'Hour',
path: createGraphPath(window.width, 400, 60),
},
{
title: 'Day',
path: createGraphPath(window.width, 400, 60),
},
{
title: 'Month',
path: createGraphPath(window.width, 400, 60),
},
{
title: 'Year',
path: createGraphPath(window.width, 400, 60),
},
];
export const Graph: React.FC<GraphProps> = ({
height = window.height,
width = window.width,
currentPath,
previousPath,
}) => {
const progress = useValue(1);
useLayoutEffect(() => {
if (currentPath === previousPath) {
return;
}
setTimeout(() => {
runTiming(progress, 1, {
duration: 300,
});
}, 200);
}, [progress, currentPath, previousPath]);
const interpolatedPath = useDerivedValue(
() => previousPath.interpolate(currentPath, progress.current),
[progress, previousPath, currentPath],
);
return (
<View style={styles.chartContainer}>
<Canvas style={styles.graph}>
<Path
path={interpolatedPath}
strokeWidth={4}
style="stroke"
strokeJoin="round"
color="black"
strokeCap="round">
<LinearGradient
start={vec(0, height * 0.5)}
end={vec(width * 0.5, height * 0.5)}
colors={['black', '#cccc66']}
/>
</Path>
</Canvas>
</View>
);
};
function GraphReanimated({
width = window.width,
currentPath,
previousPath,
}: GraphProps) {
const progress = useSharedValue(0);
const sharedPaths = useSharedValue([]);
useEffect(() => {
const strPrev = previousPath.toSVGString();
const strCurr = currentPath.toSVGString();
const paths = [parse(strPrev), parse(strCurr)];
runOnUI(() => {
sharedPaths.value = paths;
if (paths[0] !== paths[1]) {
progress.value = 0;
progress.value = withTiming(1);
}
})();
}, [currentPath, previousPath, progress, sharedPaths]);
const animatedProps = useAnimatedProps(() => {
if (!sharedPaths.value.length) {
return {
d: '',
};
}
return {
d: mixPath(progress.value, sharedPaths.value[0], sharedPaths.value[1]),
};
});
return (
<Animated.View style={styles.chartContainer}>
<Svg width={width} height={300}>
<AnimatedPath
fill="transparent"
stroke="black"
strokeWidth={3}
animatedProps={animatedProps}
/>
</Svg>
</Animated.View>
);
}
function usePrevious(value) {
const prevValue = useRef();
useEffect(() => {
prevValue.current = value;
}, [value]);
return prevValue.current;
}
export default function Interpolation() {
const [currentIndex, setIndex] = useState(0);
const currentPath = data[currentIndex];
const previousPath = data[usePrevious(currentIndex) ?? currentIndex];
return (
<View style={styles.container}>
{/* <Graph currentPath={currentPath.path} previousPath={previousPath.path} /> */}
<GraphReanimated
currentPath={currentPath.path}
previousPath={previousPath.path}
/>
<Timeline
currentIndex={currentIndex}
timelines={data}
onChange={setIndex}
/>
</View>
);
}
function Timeline({timelines, onChange, currentIndex}) {
return (
<View style={styles.timelineContainer}>
{timelines.map((item, index) => (
<TouchableOpacity key={index} onPress={() => onChange(index)}>
<View style={styles.textContainer}>
<Text
style={[styles.text, currentIndex === index && styles.active]}>
{item.title}
</Text>
</View>
</TouchableOpacity>
))}
</View>
);
}
const styles = StyleSheet.create({
container: {flex: 1, backgroundColor: 'white', justifyContent: 'center'},
chartContainer: {height: 300, width: window.width, backgroundColor: 'white'},
graph: {
flex: 1,
},
timelineContainer: {flexDirection: 'row', padding: 20, alignSelf: 'center'},
textContainer: {padding: 20},
text: {color: 'gray', fontSize: 18, fontWeight: '500'},
active: {color: 'black'},
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment