Last active
April 18, 2022 19:08
-
-
Save terrysahaidak/404d450c95a99318d8c1b9ed0fd6cd96 to your computer and use it in GitHub Desktop.
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 { | |
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