@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
JavaScript
/*
* 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);