UNPKG

@s20.ai/safecam-360-rn-lite

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

377 lines (356 loc) 11.6 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 { StyleSheet, Dimensions, LogBox, View, Text, Animated, } from 'react-native'; import FastImage from 'react-native-fast-image'; import HotspotElement from '../hotspot/HotspotElement'; import {connect} from 'react-redux'; import constants from '../../config/constants'; import {setCurrentOrientation} from '../../redux/actions/currentViewAction'; import { Gesture, GestureDetector, GestureHandlerRootView, } from 'react-native-gesture-handler'; import ProgressBar from '../common/ProgressBar'; const screen = Dimensions.get('window'); let screenWidth = screen.width; let _noOfImagesLoaded = 0; let triggerFrameInterval = null; let _currentIndex = 0; let _startX = 0; let _startRotation = 0; let _rotation = 0; let _scale = 1; LogBox.ignoreLogs(['Warning: ...']); const AnimatedFastImage = Animated.createAnimatedComponent(FastImage); const Exterior360 = ({ srcset = [], hotspotsParsed, rotationRatio = 0.25, currentHotspotState, filteredHotspotMetadata, hotspotMode, orientation, enableHotspotTooltip, hotspotPressed, customFontFamily, miniMode, }) => { const getImageWidth = () => { return orientation === constants.LANDSCAPE.toLowerCase() ? currentHotspotState ? Dimensions.get('window').width / 1.65 : Dimensions.get('window').width : Dimensions.get('window').width; }; const getImageHeight = () => { return orientation === constants.LANDSCAPE.toLowerCase() ? Dimensions.get('window').height : Dimensions.get('window').width / 1.8; }; const rotationPeriod = 360 / srcset.length; // const styles = StyleSheet.create({}); const getInitialTop = () => orientation === 'landscape' ? 0 : miniMode ? 0 : '33%'; // const noOfImagesLoaded = useSharedValue(0); // const startX = useSharedValue(0); // const startRotation = useSharedValue(0); // const rotation = useSharedValue(0); // const currentIndex = useSharedValue(0); const [noOfImagesLoaded, setNoOfImagesLoaded] = useState(0); const [currentIndexRS, setCurrentIndexRS] = useState(0); const scaleAnim = useRef(new Animated.Value(_scale)).current; const transformX = useRef(new Animated.Value(0)).current; const transformY = useRef(new Animated.Value(0)).current; const [showHotspots, setShowHotspots] = useState(true); // const scale = useSharedValue(1); // const savedScale = useSharedValue(1); // const focalX = useSharedValue(0); // const focalY = useSharedValue(0); // const hotspotOpacity = useSharedValue(1); // const imageHeightShared = useSharedValue( // orientation === 'landscape' ? '100%' : screenWidth / 1.77, // ); // const hotspotModeAlign = useSharedValue(getInitialTop()); const wrapperStyle = { position: 'relative', top: currentHotspotState ? orientation === 'landscape' ? 0 : '55%' : getInitialTop(), width: '100%', height: orientation === 'landscape' ? '100%' : screenWidth / 1.77, }; const setScale = (valueToSet, timing = false, saveScale = true) => { if (saveScale) _scale = valueToSet; if (timing) { Animated.timing(scaleAnim, { toValue: valueToSet, duration: 100, useNativeDriver: false, }).start(); } else scaleAnim.setValue(valueToSet); }; const setTransform = (ref, valueToSet, timing = false) => { if (timing) { Animated.timing(ref, { toValue: valueToSet, duration: 100, useNativeDriver: false, }).start(); } else ref.setValue(valueToSet); }; const getImageStyleRS = link => { return { position: 'absolute', width: '100%', height: orientation === 'landscape' ? '100%' : screenWidth / 1.77, opacity: srcset[currentIndexRS] === link ? 1 : 0, transform: currentHotspotState ? [] : [ {scale: scaleAnim}, {translateX: transformX}, {translateY: transformY}, ], }; }; const updateStartRotation = cf => { _rotation = cf * (360 / srcset.length); }; // useEffect(() => { // if (!currentHotspotState) { // hotspotModeAlign.value = getInitialTop(); // imageHeightShared.value = // orientation === 'landscape' ? screenHeight / 1.77 : screenWidth / 1.77; // } // }, [orientation]); useEffect(() => { if (currentHotspotState) { // hotspotModeAlign.value = withTiming( // orientation === 'landscape' ? '0%' : '55%', // ); const triggerFrame = currentHotspotState.triggerFrame; triggerFrameInterval = setInterval(() => { // logic to find the shortest path let difference = _currentIndex - triggerFrame, midPoint = srcset.length / 2, inverse = false, frameToSet = 0; if (difference > 0) { if (difference > midPoint) inverse = false; else inverse = true; } else { if (Math.abs(difference) > midPoint) inverse = true; else inverse = false; } if (!inverse) frameToSet = _currentIndex === srcset.length - 1 ? 0 : _currentIndex + 1; else frameToSet = _currentIndex === 0 ? srcset.length - 1 : _currentIndex - 1; setCurrentIndexRS(frameToSet); _currentIndex = frameToSet; if (frameToSet === triggerFrame) { updateStartRotation(frameToSet); clearInterval(triggerFrameInterval); } }, 20); } // else hotspotModeAlign.value = withTiming(getInitialTop()); }, [currentHotspotState]); const panGesture = Gesture.Pan() .maxPointers(1) .minDistance(10) .onStart(event => { _startX = event.absoluteX; _startRotation = _rotation; }) .onChange(event => { if (event.numberOfPointers === 1 && _scale === 1) { const deltaRotation = ((event.absoluteX - _startX) * 180) / (rotationRatio * screenWidth); _rotation = _startRotation - deltaRotation; const mRotation = _rotation - Math.floor(_rotation / 360) * 360; const index = Math.floor(mRotation / rotationPeriod); _currentIndex = index; setCurrentIndexRS(index); } else if (_scale > 1) { setTransform(transformX, event.translationX); setTransform(transformY, event.translationY); } }) .onEnd(event => { if (event.numberOfPointers === 1) { setTransform(transformX, 1, true); setTransform(transformY, 1, true); } }); const pinchGesture = Gesture.Pinch() .enabled(!currentHotspotState) .onChange(event => { setScale(_scale * event.scale, false, false); setShowHotspots(false); // scale.value = savedScale.value * event.scale; if (scaleAnim > 1) showHotspots(false); }) .onEnd(event => { // savedScale.value = scale.value; _scale = event.scale; if (event.velocity * -1 > 0.005) { // savedScale.value = 1; // scale.value = withTiming(1); // hotspotOpacity.value = withTiming(1); setScale(1, true); setShowHotspots(true); } else if (_scale <= 1) { // savedScale.value = 1; // scale.value = withTiming(1); // hotspotOpacity.value = withTiming(1); setScale(1, true); setShowHotspots(true); } else if (_scale >= 4) { // savedScale.value = 4; // scale.value = withTiming(4); setScale(4, true); } }); const singleTap = Gesture.Tap() .maxDuration(250) .onStart(() => { console.log('Single tap!'); }); const doubleTap = Gesture.Tap() .enabled(!currentHotspotState) .numberOfTaps(2) .onStart(() => { if (_scale <= 2) { setScale(4, true); setShowHotspots(false); // hotspotOpacity.value = withTiming(0); } else { setScale(1, true); setShowHotspots(true); // hotspotOpacity.value = withTiming(1); } }); const compose = Gesture.Race( // Gesture.Simultaneous(panGesture), Gesture.Simultaneous(pinchGesture, panGesture), Gesture.Exclusive(doubleTap, singleTap), ); const getHotspots = _ => hotspotsParsed[currentIndexRS]?.map(hotspot => ( <View collapsable={false} key={hotspot.id + Math.random()} style={{ position: 'absolute', width: '100%', height: orientation === 'landscape' ? '100%' : screenWidth / 1.77, }}> <HotspotElement customFontFamily={customFontFamily} address={ filteredHotspotMetadata.find( hp => hp.hotspotRefId === hotspot.hotspotRefId, ).address } hotspot={hotspot} hotspotPressed={hotspotPressed} hotspotLocation={constants.EXTERIOR_VIEW} hotspotType={ filteredHotspotMetadata.find( hp => hp.hotspotRefId === hotspot.hotspotRefId, ).hotspotType } enableHotspotTooltip={enableHotspotTooltip} currentHotspotState={currentHotspotState} height={getImageHeight()} orientation={orientation} width={getImageWidth()} /> </View> )); return ( <> <GestureHandlerRootView style={{ flex: miniMode ? 0 : 1, }}> <GestureDetector gesture={compose}> <View style={wrapperStyle}> {srcset.map(link => ( <AnimatedFastImage onLoad={() => { _noOfImagesLoaded += 1; if (_noOfImagesLoaded === srcset.length) { setNoOfImagesLoaded(_noOfImagesLoaded); } }} key={link} fadeDuration={0} resizeMethod="scale" resizeMode={FastImage.resizeMode.contain} source={{uri: link}} style={getImageStyleRS(link)} /> ))} {noOfImagesLoaded < srcset.length && ( <ProgressBar imageHeight={getImageHeight()} totalImages={srcset.length} noOfImagesLoaded={noOfImagesLoaded} orientation={orientation} /> )} </View> </GestureDetector> </GestureHandlerRootView> <View style={{ flex: 1, position: 'absolute', backgroundColor: 'red', top: currentHotspotState ? orientation === 'landscape' ? 0 : '55%' : getInitialTop(), height: orientation === 'landscape' ? '100%' : screenWidth / 1.77, }}> <View> {hotspotMode === constants.SHOW_ALL_HOTSPOTS && showHotspots && noOfImagesLoaded === srcset.length && getHotspots()} </View> </View> </> ); }; const mapStateToProps = ({currentView, currentHotspot, hotspotMode}) => { return { currentViewState: currentView.present, orientation: currentView.orientation.split('-')[0].toLowerCase(), widgetsVisible: currentView.widgetsVisible, currentHotspotState: currentHotspot, hotspotMode: hotspotMode, }; }; export default connect(mapStateToProps, { setCurrentOrientation, })(Exterior360);