@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
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 {
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);