Created
August 12, 2020 13:29
-
-
Save tchayen/6e746112883005a5a5d603e2adb49ee8 to your computer and use it in GitHub Desktop.
This file contains hidden or 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 {Dimensions, PixelRatio, StyleSheet, Text, View} from 'react-native'; | |
import Animated, { | |
useSharedValue, | |
useAnimatedGestureHandler, | |
useAnimatedStyle, | |
useAnimatedProps, | |
useDerivedValue, | |
} from 'react-native-reanimated'; | |
import {PanGestureHandler, TextInput} from 'react-native-gesture-handler'; | |
import Svg, {Circle, Defs, Stop, LinearGradient} from 'react-native-svg'; | |
const BACKGROUND_COLOR = 'rgb(255, 255, 255)'; | |
const CIRCLE_BACKGROUND = 'rgb(150, 150, 150)'; | |
const TEXT_COLOR = '#000'; | |
const SECONDARY_TEXT_COLOR = '#777'; | |
const GRADIENT_START = '#eda338'; | |
const GRADIENT_STOP = '#f5d346'; | |
const {width} = Dimensions.get('screen'); | |
const size = width - 32; | |
const STROKE_WIDTH = 44; | |
const r = PixelRatio.roundToNearestPixel(size / 2); | |
const clamp = (value, lowerBound, upperBound) => { | |
'worklet'; | |
return Math.min(Math.max(lowerBound, value), upperBound); | |
}; | |
const lerp = (x, y, value) => { | |
'worklet'; | |
return x * (1 - value) + y * value; | |
}; | |
const invlerp = (x, y, value) => { | |
'worklet'; | |
return clamp((value - x) / (y - x), 0, 1); | |
}; | |
const range = (x1, y1, x2, y2, value) => { | |
'worklet'; | |
return lerp(x2, y2, invlerp(x1, y1, value)); | |
}; | |
const canvasToCartesian = (v, center) => { | |
'worklet'; | |
return { | |
x: v.x - center.x, | |
y: -1 * (v.y - center.y), | |
}; | |
}; | |
const cartesianToPolar = (v) => { | |
'worklet'; | |
return { | |
theta: Math.atan2(v.y, v.x), | |
radius: Math.sqrt(v.x ** 2 + v.y ** 2), | |
}; | |
}; | |
const polarToCartesian = (p) => { | |
'worklet'; | |
return { | |
x: p.radius * Math.cos(p.theta), | |
y: p.radius * Math.sin(p.theta), | |
}; | |
}; | |
const cartesianToCanvas = (v, center) => { | |
'worklet'; | |
return { | |
x: v.x + center.x, | |
y: -1 * v.y + center.y, | |
}; | |
}; | |
export const polarToCanvas = (p, center) => { | |
'worklet'; | |
return cartesianToCanvas(polarToCartesian(p), center); | |
}; | |
export const canvasToPolar = (v, center) => { | |
'worklet'; | |
return cartesianToPolar(canvasToCartesian(v, center)); | |
}; | |
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput); | |
const ReText = (props) => { | |
const {text, style} = {style: {}, ...props}; | |
const animatedProps = useAnimatedProps(() => { | |
return { | |
text: text.value, | |
}; | |
}); | |
return ( | |
<AnimatedTextInput | |
underlineColorAndroid="transparent" | |
editable={false} | |
value={text.value} | |
{...{style, animatedProps}} | |
/> | |
); | |
}; | |
const styles = StyleSheet.create({ | |
container: { | |
flex: 1, | |
paddingTop: 40, | |
alignItems: 'center', | |
backgroundColor: BACKGROUND_COLOR, | |
}, | |
content: { | |
width: r * 2, | |
height: r * 2, | |
}, | |
}); | |
const Label = ({input}) => { | |
const data = useDerivedValue(() => { | |
const time = (12 - range(0, Math.PI * 2, 0, 12, input.value) + 3) % 12; | |
const hours = Math.floor(time); | |
const minutes = Math.floor((time - hours) * 60); | |
const pad = (number) => String(number).padStart(2, '0'); | |
return `${pad(hours)}:${pad(minutes)}`; | |
}); | |
return ( | |
<View> | |
<ReText style={{fontSize: 32, color: TEXT_COLOR}} text={data} /> | |
</View> | |
); | |
}; | |
const AnimatedCircle = Animated.createAnimatedComponent(Circle); | |
const CircularProgress = ({a, b}) => { | |
const radius = r - STROKE_WIDTH / 2; | |
const circumference = radius * 2 * Math.PI; | |
const strokeDashoffset = useAnimatedProps(() => { | |
const percentage = | |
((b.value < a.value ? Math.PI * 2 : 0) + b.value - a.value) / Math.PI / 2; | |
return { | |
strokeDashoffset: circumference * percentage, | |
}; | |
}); | |
const style = useAnimatedStyle(() => { | |
return { | |
transform: [{rotateZ: `-${a.value}rad`}], | |
}; | |
}); | |
return ( | |
<Animated.View style={[StyleSheet.absoluteFill, style]}> | |
<Svg style={StyleSheet.absoluteFill}> | |
<Defs> | |
<LinearGradient id="grad" x1="0" y1="0" x2="100%" y2="0"> | |
<Stop offset="0%" stopColor={GRADIENT_START} /> | |
<Stop offset="100%" stopColor={GRADIENT_STOP} /> | |
</LinearGradient> | |
</Defs> | |
<Circle | |
stroke={CIRCLE_BACKGROUND} | |
strokeWidth={STROKE_WIDTH} | |
r={radius} | |
cx={r} | |
cy={r} | |
/> | |
<AnimatedCircle | |
stroke="url(#grad)" | |
fill="none" | |
strokeWidth={STROKE_WIDTH} | |
r={radius} | |
cx={r} | |
cy={r} | |
strokeDasharray={`${circumference} ${circumference}`} | |
animatedProps={strokeDashoffset} | |
/> | |
</Svg> | |
</Animated.View> | |
); | |
}; | |
const Circular = ({theta, fill}) => { | |
const radius = r - STROKE_WIDTH / 2; | |
const center = {x: radius, y: radius}; | |
const onGestureEvent = useAnimatedGestureHandler({ | |
onActive: (event, ctx) => { | |
const {translationX, translationY} = event; | |
const x = ctx.offset.x + translationX; | |
const y = ctx.offset.y + translationY; | |
const value = canvasToPolar({x, y}, center).theta; | |
theta.value = value > 0 ? value : 2 * Math.PI + value; | |
}, | |
onStart: (_, ctx) => { | |
ctx.offset = polarToCanvas({theta: theta.value, radius: radius}, center); | |
}, | |
}); | |
const style = useAnimatedStyle(() => { | |
const {x: translateX, y: translateY} = polarToCanvas( | |
{ | |
theta: theta.value, | |
radius: radius, | |
}, | |
center, | |
); | |
return { | |
transform: [{translateX}, {translateY}], | |
}; | |
}); | |
return ( | |
<PanGestureHandler {...{onGestureEvent}}> | |
<Animated.View | |
style={[ | |
{ | |
...StyleSheet.absoluteFillObject, | |
width: STROKE_WIDTH, | |
height: STROKE_WIDTH, | |
borderRadius: STROKE_WIDTH / 2, | |
backgroundColor: fill, | |
}, | |
style, | |
]} | |
/> | |
</PanGestureHandler> | |
); | |
}; | |
const Bedtime = () => { | |
const a = useSharedValue(0.5 * Math.PI); | |
const b = useSharedValue(1.25 * Math.PI); | |
return ( | |
<View style={styles.container}> | |
<Animated.View | |
style={{ | |
width: '100%', | |
paddingHorizontal: 16, | |
marginBottom: 32, | |
flexDirection: 'row', | |
justifyContent: 'space-between', | |
}}> | |
<View> | |
<Text style={{fontSize: 16, color: SECONDARY_TEXT_COLOR}}> | |
Bedtime | |
</Text> | |
<Label input={a} /> | |
</View> | |
<View style={{alignItems: 'flex-end'}}> | |
<Text style={{fontSize: 16, color: SECONDARY_TEXT_COLOR}}>Wake</Text> | |
<Label input={b} /> | |
</View> | |
</Animated.View> | |
<View style={styles.content}> | |
<Animated.View style={StyleSheet.absoluteFill}> | |
<CircularProgress a={a} b={b} /> | |
</Animated.View> | |
<Circular theta={a} fill="#000" /> | |
<Circular theta={b} fill="#fff" /> | |
</View> | |
</View> | |
); | |
}; | |
export default Bedtime; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment