import React, { useState } from 'react'; import { Alert, StyleSheet, useWindowDimensions } from 'react-native'; import { GestureDetector, Gesture } from 'react-native-gesture-handler'; import Animated, { useSharedValue, useAnimatedStyle, withTiming, runOnJS, } from 'react-native-reanimated'; const filters = ['red', 'green', 'blue', 'yellow', 'orange', 'cyan']; const MAX_VIDEO_DURATION_MS = 60_000; const CAPTURE_BUTTON_RADIUS = 50; const FILTER_BUTTON_RADIUS = 35; export default function Home() { const filter = useSharedValue(0); const filterOffset = useSharedValue(0); const zoom = useSharedValue(1); const [scale, setScale] = useState(1); const [selectedFilter, setSelectedFilter] = useState(0); const [isRecording, setIsRecording] = useState(false); const [remainingTimeMs, setRemainingTimeMs] = useState(MAX_VIDEO_DURATION_MS); const [recordingIntervalHandle, setRecordingIntervalHandle] = useState(-1); const filtersPanGesture = Gesture.Pan() .onUpdate((e) => { 'worklet'; filter.value = filter.value + (filterOffset.value - e.translationX) / 100; filterOffset.value = e.translationX; runOnJS(updateSelectedFilter)(); }) .onEnd(() => { 'worklet'; filterOffset.value = 0; runOnJS(stopFilterScroll)(); }); const buttonTapGesture = Gesture.Tap().onEnd((_e, success) => { 'worklet'; if (success) { runOnJS(takePhoto)(); } }); const buttonDoubleTapGesture = Gesture.Tap() .numberOfTaps(2) .onEnd((_e, success) => { 'worklet'; if (success) { runOnJS(takeSeries)(); } }); const buttonPanGesture = Gesture.Pan() .simultaneousWithExternalGesture(filtersPanGesture) .onUpdate((e) => { 'worklet'; if (isRecording) { if (e.velocityY < 0) { zoom.value *= 1.05; } else if (e.velocityY > 0) { zoom.value *= 0.95; } } }); const buttonLongPressGesture = Gesture.LongPress() .maxDistance(10000) .onStart(() => { 'worklet'; runOnJS(startRecording)(); }) .onEnd(() => { 'worklet'; if (isRecording) { runOnJS(finishRecording)(); } }); const previewPinchGesture = Gesture.Pinch() .onStart(() => { 'worklet'; runOnJS(setScale)(zoom.value); }) .onUpdate((e) => { 'worklet'; zoom.value = scale * e.scale; }); const buttonGesture = Gesture.Simultaneous( buttonLongPressGesture, Gesture.Exclusive( buttonPanGesture, buttonDoubleTapGesture, buttonTapGesture ) ); function stopFilterScroll() { filter.value = withTiming(updateSelectedFilter(), { duration: 200 }); } function updateSelectedFilter() { const selectedFilter = Math.round( Math.min(filters.length - 1, Math.max(filter.value, 0)) ); setSelectedFilter(selectedFilter); return selectedFilter; } function takePhoto() { Alert.alert('You took a photo'); } function takeSeries() { Alert.alert('You took a series of photos'); } function startRecording() { setIsRecording(true); setRemainingTimeMs(MAX_VIDEO_DURATION_MS); setRecordingIntervalHandle( setInterval(() => { setRemainingTimeMs((r) => r - 200); }, 200) ); } function finishRecording() { setIsRecording(false); clearInterval(recordingIntervalHandle); setRemainingTimeMs(MAX_VIDEO_DURATION_MS); Alert.alert( `You took a video (${(MAX_VIDEO_DURATION_MS - remainingTimeMs) / 1000} s)` ); } const zoomStyle = useAnimatedStyle(() => { return { transform: [{ scale: zoom.value }] }; }); return ( { finishRecording(); }} /> ); } const styles = StyleSheet.create({ container: { width: '100%', height: '100%', alignSelf: 'center', backgroundColor: 'white', }, home: { width: '100%', height: '100%', alignSelf: 'center', opacity: 0.5, }, buttonContainer: { width: '100%', height: CAPTURE_BUTTON_RADIUS * 2, position: 'absolute', bottom: 50, zIndex: 10, }, box: { alignSelf: 'center', margin: 100, width: 200, height: 200, backgroundColor: 'black', }, }); function FilterCarousel(props: { filters: string[]; selected: Animated.SharedValue; }) { return ( {props.filters.map((filter) => ( ))} ); } function Filter(props: { filter: string; selected: Animated.SharedValue; }) { const window = useWindowDimensions(); const style = useAnimatedStyle(() => { return { transform: [ { translateX: window.width / 2 - CAPTURE_BUTTON_RADIUS - CAPTURE_BUTTON_RADIUS * 2 * props.selected.value, }, ], }; }); return ( ); } const filterCarouselStyles = StyleSheet.create({ container: { width: '100%', height: CAPTURE_BUTTON_RADIUS * 2, alignSelf: 'center', position: 'absolute', flexDirection: 'row', }, filter: { width: FILTER_BUTTON_RADIUS * 2, height: FILTER_BUTTON_RADIUS * 2, borderRadius: FILTER_BUTTON_RADIUS * 2, alignSelf: 'flex-start', margin: CAPTURE_BUTTON_RADIUS - FILTER_BUTTON_RADIUS, borderWidth: 0.5, borderColor: 'gray', }, }); function CaptureButton(props: { progress: number; onTimerFinished: () => void; }) { function getOverlay(progress: number) { if (progress > 1) { progress = 1; props.onTimerFinished?.(); } const progressBelowHalf = progress <= 0.5; return ( ); } return ( {getOverlay(props.progress)} ); } const captureButtonStyles = StyleSheet.create({ container: { width: CAPTURE_BUTTON_RADIUS * 2, height: CAPTURE_BUTTON_RADIUS * 2, alignSelf: 'center', borderWidth: 8, borderRadius: CAPTURE_BUTTON_RADIUS * 2, borderColor: 'white', alignItems: 'center', justifyContent: 'center', position: 'absolute', }, progress: { width: CAPTURE_BUTTON_RADIUS * 2, height: CAPTURE_BUTTON_RADIUS * 2, borderWidth: 8, borderRadius: CAPTURE_BUTTON_RADIUS, position: 'absolute', borderLeftColor: 'transparent', borderBottomColor: 'transparent', borderTopColor: 'red', borderRightColor: 'red', transform: [{ rotateZ: '45deg' }], }, overlay: { width: CAPTURE_BUTTON_RADIUS * 2, height: CAPTURE_BUTTON_RADIUS * 2, borderWidth: 8, borderRadius: CAPTURE_BUTTON_RADIUS, position: 'absolute', }, overlayLessThanHalf: { borderLeftColor: 'transparent', borderBottomColor: 'transparent', borderTopColor: 'white', borderRightColor: 'white', }, overlayMoreThanHalf: { borderLeftColor: 'transparent', borderBottomColor: 'transparent', borderTopColor: 'red', borderRightColor: 'red', }, });