UNPKG

@s20.ai/safecam-360-rn

Version:

A React Native component to view the interior panorama and exterior 360 degree views of a product featuring suppport for hotspots. Compatible with Android and iOS

351 lines (333 loc) 10.3 kB
/* * Copyright © 2022 S20.AI India Pvt. Ltd. * Contact - origin@s20.ai * License - Licensed as per https://docs.s20.ai/license.html */ import React, {useEffect, useRef, useState} from 'react'; import { View, Text, StyleSheet, Dimensions, PanResponder, LogBox, Image, } from 'react-native'; import FastImage from 'react-native-fast-image'; import HotspotElement from '../hotspot/Element'; import {connect} from 'react-redux'; import constants from '../../config/constants'; import {setCurrentOrientation} from '../../redux/actions/currentViewAction'; import { GestureHandlerRootView, PanGestureHandler, PinchGestureHandler, State, TapGestureHandler, } from 'react-native-gesture-handler'; import Animated, { event, useAnimatedGestureHandler, useAnimatedStyle, withTiming, useSharedValue, } from 'react-native-reanimated'; const screen = Dimensions.get('screen'); let screenWidth = screen.width; let screenHeight = screen.height; const AnimatedFastImage = Animated.createAnimatedComponent(FastImage); const Exterior360 = ({ width, // height = 300, srcset = [], hotspots = [], rotationRatio = 0.5, currentHotspotState, filteredHotspotMetadata, currentViewState, hotspotMode, orientationState, }) => { // const [currentIndex, setCurrentIndex] = useState(0); // const [startX, setStartX] = useState(0); // const [startRotation, setStartRotation] = useState(0); // const [rotation, setRotation] = useState(0); const pinchRef = useRef(null); const panRef = useRef(null); // const [scale, setScale] = useState(1); // const [focalX, setFocalX] = useState(0); // const [focalY, setFocalY] = useState(0); // const [oldScale, setOldScale] = useState(0); const rotationPeriod = 360 / srcset.length; const getImageStyle = link => { screenWidth = Dimensions.get('screen').width; screenHeight = Dimensions.get('screen').height; return useAnimatedStyle(() => { // if (srcset[currentIndex.value] === link) console.log(); return { // top: '50%', // marginTop: '50%', position: 'absolute', width: currentHotspotState && orientationState === 'landscape' ? screenWidth / 2 : screenWidth, height: orientationState === 'portrait' ? screenWidth / 1.9 : screenHeight, opacity: srcset[currentIndex.value] === link ? 1 : 0, }; }); }; const styles = StyleSheet.create({ container: { height: '100%', width: '100%', }, wrapper: { top: '33%', flex: 1, }, landscapeWrapper: { top: 0, flex: 1, }, containerHotspotMode: { flex: 1, marginTop: '50%', // zIndex: 10000, // elevation: 1000, // top: '50%' }, containerHotspotModeLandScape: { top: 0, flex: 1, width: currentHotspotState ? '50%' : '100%', zindex: 10, }, }); const startX = useSharedValue(0); const startRotation = useSharedValue(0); const rotation = useSharedValue(0); const currentIndex = useSharedValue(0); const scale = useSharedValue(1); const focalX = useSharedValue(0); const focalY = useSharedValue(0); const oldScale = useSharedValue(0); const hotspotOpacity = useSharedValue(1); const halfheight = Dimensions.get('window').height / 3; const hotspotModeAlign = useSharedValue('0%'); const handleOrientationChange = () => { const {height, width} = Dimensions.get('screen'); if (height >= width) setCurrentOrientation('portrait'); else setCurrentOrientation('landscape'); }; useEffect(() => { let dimensionsListener = Dimensions.addEventListener( 'change', handleOrientationChange, ); return () => dimensionsListener.remove(); }, []); const panHandler = useAnimatedGestureHandler({ onStart: event => { startX.value = event.absoluteX; startRotation.value = rotation.value; }, onActive: event => { if (event.numberOfPointers === 1) { if (scale.value === 1) { const deltaRotation = ((event.absoluteX - startX.value) * 180) / (rotationRatio * screenWidth); rotation.value = startRotation.value - deltaRotation; const mRotation = rotation.value - Math.floor(rotation.value / 360) * 360; const index = Math.floor(mRotation / rotationPeriod); currentIndex.value = index; } else if (scale.value > 1) { focalX.value = event.translationX; focalY.value = event.translationY; } } }, onEnd: event => { if (event.numberOfPointers === 1) { focalX.value = withTiming(1); focalY.value = withTiming(1); } }, }); const pinchHandler = useAnimatedGestureHandler({ onStart: event => { oldScale.value = event.scale; }, onActive: event => { scale.value = scale.value + event.scale - oldScale.value; if (scale.value > 1) hotspotOpacity.value = withTiming(0); oldScale.value = event.scale; // if (scale !== 1) { // focalX.value = event.focalX; // focalY.value = event.focalY; // } }, onEnd: event => { if (event.velocity * -1 > 0.003) { scale.value = withTiming(1); hotspotOpacity.value = withTiming(1); } oldScale.value = 0; if (scale.value <= 1) { scale.value = withTiming(1); hotspotOpacity.value = withTiming(1); } }, }); useEffect(() => { if (currentHotspotState) hotspotModeAlign.value = withTiming('30%'); else hotspotModeAlign.value = withTiming('0%'); }, [currentHotspotState]); const hStyle = frameId => useAnimatedStyle(() => { return { opacity: frameId === currentIndex.value ? hotspotOpacity.value : 0, }; }); const rStyle = useAnimatedStyle(() => { return { position: 'absolute', zIndex: 100, elevation: 100, // top: hotspotModeAlign.value, // top: currentHotspotState ? '50%' : 'auto', transform: [ {scale: scale.value}, {translateX: focalX.value}, {translateY: focalY.value}, // {translateX: -width / 2}, // {translateY: -height / 2}, // {translateX: -focalX.value}, // {translateY: -focalY.value}, // {translateX: width / 2}, // {translateY: height / 2}, ], }; }); const wrapperStyle = useAnimatedStyle(() => { return { position: 'absolute', top: hotspotModeAlign.value, flex: 1, }; }); useEffect(() => { if (currentHotspotState) { let myInterval = setInterval(() => { if (currentIndex.value < currentHotspotState.triggerFrame) currentIndex.value = currentIndex.value + 2; else clearInterval(myInterval); }, 1); } }, [currentHotspotState]); return ( <GestureHandlerRootView // style={ // currentHotspotState ? styles.containerHotspotMode : styles.container // // {flex: 1} // // [wrapperStyle] // } style={{ ...(orientationState === 'landscape' ? styles.containerHotspotModeLandScape : currentHotspotState ? styles.containerHotspotMode : styles.container), }}> <Animated.View style={{flex: 1}} // style={[wrapperStyle]} > <Animated.View style={[ orientationState === 'landscape' ? styles.landscapeWrapper : styles.wrapper, ]}> <PanGestureHandler onGestureEvent={panHandler} // onHandlerStateChange={stateChangeHandler} ref={panRef} // minDist={75} minPointers={1} maxPointers={1} shouldCancelWhenOutside={true} simultaneousHandlers={pinchRef}> <Animated.View style={{flex: 1}}> {srcset.map(link => ( <AnimatedFastImage key={link} fadeDuration={0} resizeMethod="scale" resizeMode={FastImage.resizeMode.cover} source={{uri: link}} style={[ getImageStyle(link), // { // transform: [ // {scale: scale}, // {translateX: focalX}, // {translateY: focalY}, // ], // }, // rStyle, ]} /> ))} </Animated.View> </PanGestureHandler> {hotspotMode === constants.SHOW_ALL_HOTSPOTS && // scale <= 1 && hotspots // .filter( // d => // d.frameId === // srcset.findIndex(d => d === srcset[currentIndex.value]), // ) .map(hotspot => ( <Animated.View key={hotspot.id + Math.random()} style={[ { position: 'absolute', // marginTop: currentHotspotState ? '63%' : 'auto', }, hStyle(hotspot.frameId), ]}> <HotspotElement address={ filteredHotspotMetadata.find( hp => hp.hotspotRefId === hotspot.hotspotRefId, ).address } hotspot={hotspot} width={ orientationState === 'landscape' && currentHotspotState ? screenWidth / 2 : screenWidth } /> </Animated.View> ))} </Animated.View> </Animated.View> </GestureHandlerRootView> ); }; const mapStateToProps = ({currentView, currentHotspot, hotspotMode}) => { return { currentViewState: currentView.present, currentHotspotState: currentHotspot, hotspotMode: hotspotMode, orientationState: currentView.orientation, }; }; export default connect(mapStateToProps, {setCurrentOrientation})(Exterior360);