Skip to content

Instantly share code, notes, and snippets.

@tchayen
Created Aug 12, 2020
Embed
What would you like to do?
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