├── .DS_Store ├── .circleci └── config.yml ├── .codecov.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── Readme.md ├── babel.config.js ├── examples ├── .DS_Store └── react │ ├── .DS_Store │ ├── build │ ├── asset-manifest.json │ ├── favicon.ico │ ├── index.html │ ├── manifest.json │ ├── precache-manifest.0e97756a53cf69ed95feacbcdcd4fabc.js │ ├── service-worker.js │ └── static │ │ └── js │ │ ├── 2.0b89f304.chunk.js │ │ ├── 2.0b89f304.chunk.js.LICENSE.txt │ │ ├── 2.0b89f304.chunk.js.map │ │ ├── main.a4568c0c.chunk.js │ │ ├── main.a4568c0c.chunk.js.map │ │ ├── runtime-main.7bb332b4.js │ │ └── runtime-main.7bb332b4.js.map │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json │ └── src │ ├── .DS_Store │ ├── App.js │ ├── Component │ ├── .DS_Store │ ├── ThreeSixtyHotspots.js │ ├── ThreeSixtyViewer.js │ └── ZoomPan.js │ ├── constants.js │ ├── index.js │ └── utils │ └── updateHotspots.js ├── package-lock.json ├── package.json ├── rollup.config.js └── src ├── .DS_Store ├── components ├── .DS_Store ├── ThreeSixtyHotspots.js ├── ThreeSixtyViewer.js └── ZoomPan.js ├── index.js └── utils └── updateHotspots.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashivliving/react-threesixty/f82461ef9b5bdc88c45744b7a57b837d87ecf0cb/.DS_Store -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | node: circleci/node@1.1.6 4 | jobs: 5 | build-and-test: 6 | executor: 7 | name: node/default 8 | steps: 9 | - checkout 10 | - node/with-cache: 11 | steps: 12 | - run: npm install 13 | - run: npm test 14 | workflows: 15 | build-and-test: 16 | jobs: 17 | - build-and-test 18 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: yes 3 | comment: 4 | layout: "reach, diff, flags, files" 5 | behavior: default 6 | require_changes: false 7 | branches: 8 | - "master" 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | [*] 3 | charset = utf-8 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "node": true, 5 | "jest": true 6 | }, 7 | "extends": [ 8 | "standard", 9 | "standard-react", 10 | "plugin:prettier/recommended", 11 | "prettier/standard", 12 | "prettier/react" 13 | ], 14 | "parserOptions": { 15 | "ecmaVersion": 2020, 16 | "ecmaFeatures": { 17 | "legacyDecorators": true, 18 | "jsx": true 19 | } 20 | }, 21 | "settings": { 22 | "react": { 23 | "version": "16" 24 | } 25 | }, 26 | "rules": { 27 | "space-before-function-paren": 0, 28 | "react/prop-types": 0, 29 | "react/jsx-handler-names": 0, 30 | "react/jsx-fragments": 0, 31 | "react/no-unused-prop-types": 0, 32 | "import/export": 0 33 | } 34 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | coverage 3 | node_modules 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "semi": false, 5 | "tabWidth": 2, 6 | "bracketSpacing": true, 7 | "jsxBracketSameLine": false, 8 | "arrowParens": "always", 9 | "trailingComma": "none" 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "prettier.eslintIntegration": true 4 | } 5 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | ### Install 2 | 3 | ThreeSixty is available as NPM package 4 | 5 | ``` 6 | npm i react-threesixty 7 | ``` 8 | 9 | ### Example 10 | 11 | ```js 12 | 13 | 18 | ``` 19 | 20 | ### Options 21 | 22 | ```js 23 | { 24 | // Source image url object with 25 | imageArr: [{ 26 | 'image_url' : 'images/example-1.jpeg' 27 | }, { 28 | 'image_url' : 'images/example-2.jpeg' 29 | }, { 30 | 'image_url' : 'images/example-3.jpeg' 31 | } 32 | ... 33 | ], // Also supports passing an array of images 34 | isMobile : false, // if it is mobile. Default : false 35 | imageKey : 'image_url', // imageArr key for imageUrl. Default : image_url 36 | zoomImageKey: 'zoom_image_url', 37 | // Width & Height 38 | width: 300, // Image width. Default 300 39 | height: 300, // Image height. Default 300 40 | updateIndex: 0, // Update 360 Index. Default initialize to 0 41 | startIndex: 0, // Start Index. Default 0 42 | 43 | // Navigation 44 | prev: document.getElementById('prev'), // Previous button element. Default: null 45 | next: document.getElementById('next'), // Next button element. Default: null 46 | keys: true, // Rotate image on arrow keys. Default: true 47 | draggable: true, // Rotate image by dragging. Default: true 48 | swipeable: true, // Rotate image by swiping on mobile screens. Default: true 49 | dragTolerance: 10, // Rotation speed when dragging. Default: 10 50 | swipeTolerance: 10, // Rotation speed when swiping. Default: 10 51 | swipeTarget: document.getElementById('wrapper'), // Element which will listen for drag/swipe events. Default: Image container 52 | 53 | // Rotation settings 54 | speed: 100, // Rotation speed during 'play' mode. Default: 10 55 | inverted: false, // Inverts rotation direction 56 | autoPlay: false, // Initial Autoplay. Default: false 57 | containerName: 'reactThreesixtyContainer' // Three sixty container name. Default: 'reactThreesixtyContainer' 58 | handleImageChange: null // Callback function to get image change. Default: null. Returns new image index 59 | } 60 | ``` 61 | 62 | ### Array of images 63 | 64 | As an alternative to sprite image, ThreeSixty also supports using array of images: 65 | 66 | ```js 67 | 78 | ``` 79 | 80 | ### Licence 81 | 82 | Licensed under the MIT license. 83 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | const presets = [ 4 | ["@babel/preset-env", { 5 | "modules": false, 6 | "bugfixes": true, 7 | "targets": { "browsers": "> 0.25%, ie 11, not op_mini all, not dead" } 8 | }], 9 | "@babel/preset-react" 10 | ]; 11 | const plugins = ['macros', "babel-plugin-styled-components"]; 12 | 13 | return { 14 | presets, 15 | plugins 16 | } 17 | } -------------------------------------------------------------------------------- /examples/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashivliving/react-threesixty/f82461ef9b5bdc88c45744b7a57b837d87ecf0cb/examples/.DS_Store -------------------------------------------------------------------------------- /examples/react/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashivliving/react-threesixty/f82461ef9b5bdc88c45744b7a57b837d87ecf0cb/examples/react/.DS_Store -------------------------------------------------------------------------------- /examples/react/build/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.js": "/static/js/main.a4568c0c.chunk.js", 4 | "main.js.map": "/static/js/main.a4568c0c.chunk.js.map", 5 | "runtime-main.js": "/static/js/runtime-main.7bb332b4.js", 6 | "runtime-main.js.map": "/static/js/runtime-main.7bb332b4.js.map", 7 | "static/js/2.0b89f304.chunk.js": "/static/js/2.0b89f304.chunk.js", 8 | "static/js/2.0b89f304.chunk.js.map": "/static/js/2.0b89f304.chunk.js.map", 9 | "index.html": "/index.html", 10 | "precache-manifest.0e97756a53cf69ed95feacbcdcd4fabc.js": "/precache-manifest.0e97756a53cf69ed95feacbcdcd4fabc.js", 11 | "service-worker.js": "/service-worker.js", 12 | "static/js/2.0b89f304.chunk.js.LICENSE.txt": "/static/js/2.0b89f304.chunk.js.LICENSE.txt" 13 | }, 14 | "entrypoints": [ 15 | "static/js/runtime-main.7bb332b4.js", 16 | "static/js/2.0b89f304.chunk.js", 17 | "static/js/main.a4568c0c.chunk.js" 18 | ] 19 | } -------------------------------------------------------------------------------- /examples/react/build/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashivliving/react-threesixty/f82461ef9b5bdc88c45744b7a57b837d87ecf0cb/examples/react/build/favicon.ico -------------------------------------------------------------------------------- /examples/react/build/index.html: -------------------------------------------------------------------------------- 1 | React Ui
-------------------------------------------------------------------------------- /examples/react/build/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "react-threesixty", 3 | "name": "react-threesixty", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } -------------------------------------------------------------------------------- /examples/react/build/precache-manifest.0e97756a53cf69ed95feacbcdcd4fabc.js: -------------------------------------------------------------------------------- 1 | self.__precacheManifest = (self.__precacheManifest || []).concat([ 2 | { 3 | "revision": "2409815e3f520ce7d6bbbb2c8681e32c", 4 | "url": "/index.html" 5 | }, 6 | { 7 | "revision": "3218eea32f2dd144580b", 8 | "url": "/static/js/2.0b89f304.chunk.js" 9 | }, 10 | { 11 | "revision": "e88a3e95b5364d46e95b35ae8c0dc27d", 12 | "url": "/static/js/2.0b89f304.chunk.js.LICENSE.txt" 13 | }, 14 | { 15 | "revision": "bd60842f9b5753e1c058", 16 | "url": "/static/js/main.a4568c0c.chunk.js" 17 | }, 18 | { 19 | "revision": "08f73d5cd9e484c26cef", 20 | "url": "/static/js/runtime-main.7bb332b4.js" 21 | } 22 | ]); -------------------------------------------------------------------------------- /examples/react/build/service-worker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Welcome to your Workbox-powered service worker! 3 | * 4 | * You'll need to register this file in your web app and you should 5 | * disable HTTP caching for this file too. 6 | * See https://goo.gl/nhQhGp 7 | * 8 | * The rest of the code is auto-generated. Please don't update this file 9 | * directly; instead, make changes to your Workbox build configuration 10 | * and re-run your build process. 11 | * See https://goo.gl/2aRDsh 12 | */ 13 | 14 | importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js"); 15 | 16 | importScripts( 17 | "/precache-manifest.0e97756a53cf69ed95feacbcdcd4fabc.js" 18 | ); 19 | 20 | self.addEventListener('message', (event) => { 21 | if (event.data && event.data.type === 'SKIP_WAITING') { 22 | self.skipWaiting(); 23 | } 24 | }); 25 | 26 | workbox.core.clientsClaim(); 27 | 28 | /** 29 | * The workboxSW.precacheAndRoute() method efficiently caches and responds to 30 | * requests for URLs in the manifest. 31 | * See https://goo.gl/S9QRab 32 | */ 33 | self.__precacheManifest = [].concat(self.__precacheManifest || []); 34 | workbox.precaching.precacheAndRoute(self.__precacheManifest, {}); 35 | 36 | workbox.routing.registerNavigationRoute(workbox.precaching.getCacheKeyForURL("/index.html"), { 37 | 38 | blacklist: [/^\/_/,/\/[^\/?]+\.[^\/]+$/], 39 | }); 40 | -------------------------------------------------------------------------------- /examples/react/build/static/js/2.0b89f304.chunk.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /** @license React v0.19.1 8 | * scheduler.production.min.js 9 | * 10 | * Copyright (c) Facebook, Inc. and its affiliates. 11 | * 12 | * This source code is licensed under the MIT license found in the 13 | * LICENSE file in the root directory of this source tree. 14 | */ 15 | 16 | /** @license React v16.13.1 17 | * react-dom.production.min.js 18 | * 19 | * Copyright (c) Facebook, Inc. and its affiliates. 20 | * 21 | * This source code is licensed under the MIT license found in the 22 | * LICENSE file in the root directory of this source tree. 23 | */ 24 | 25 | /** @license React v16.13.1 26 | * react.production.min.js 27 | * 28 | * Copyright (c) Facebook, Inc. and its affiliates. 29 | * 30 | * This source code is licensed under the MIT license found in the 31 | * LICENSE file in the root directory of this source tree. 32 | */ 33 | -------------------------------------------------------------------------------- /examples/react/build/static/js/main.a4568c0c.chunk.js: -------------------------------------------------------------------------------- 1 | (this.webpackJsonpreact=this.webpackJsonpreact||[]).push([[0],{10:function(a,e,n){a.exports=n(15)},15:function(a,e,n){"use strict";n.r(e);var i=n(0),r=n.n(i),o=n(6),s=n.n(o),m=n(1),t=n(8),d=n(9),c=n(7),u=n.n(c),l=n(4),g=Object(i.memo)((function(a){var e=a.imageArr,n=a.imageKey,o=void 0===n?"image_url":n,s=a.type,c=void 0===s?"exterior":s,g=a.autoPlay,p=a.startIndex,w=void 0===p?0:p,f=a.updateIndex,h=a.handleImageChange,b=a.containerName,y=void 0===b?"reactThreesixtyContainer":b,G=Object(i.useRef)(null),z=Object(i.useRef)(null),P=Object(i.useState)(!1),k=Object(m.a)(P,2),J=k[0],Q=k[1],S=Object(i.useState)([]),B=Object(m.a)(S,2),v=B[0],E=B[1],O=Object(i.useState)(!1),R=Object(m.a)(O,2),x=R[0],A=R[1],L=function(a,e){var n=0,i=a.length;a.forEach((function(a){!function(a,e){var n=new Image;n.onload=e,n.src=a}(a,(function(){++n===i&&e()}))}))},T=function(a){a&&a.detail&&h&&h({index:a.detail.image_index,item:e[a.detail.image_index]})},j=function(){Q(!0)},_=function(){Q(!1)};return Object(i.useEffect)((function(){z.current&&f>=0&&f {\n const { imageArr, imageKey = 'image_url', type='exterior', autoPlay, startIndex=0, updateIndex, handleImageChange, containerName = 'reactThreesixtyContainer' } = props;\n const viewerRef = useRef(null);\n const threeSixtyRef = useRef(null);\n const [dragState, setDragState] = useState(false);\n const [loadedType, setLoadedType] = useState([]);\n const [allImagesLoaded, setAllImagesLoaded] = useState(false);\n\n const preloadImages = (urls, allImagesLoadedCallback) => {\n var loadedCounter = 0;\n var toBeLoadedNumber = urls.length;\n urls.forEach(function (url) {\n preloadImage(url, function () {\n loadedCounter++;\n if (loadedCounter === toBeLoadedNumber) {\n allImagesLoadedCallback();\n }\n });\n });\n function preloadImage(url, anImageLoadedCallback) {\n var img = new Image();\n img.onload = anImageLoadedCallback;\n img.src = url;\n }\n }\n\n const imageChange = (e) => {\n if (e && e.detail && handleImageChange) {\n handleImageChange({\n index: e.detail.image_index,\n item: imageArr[e.detail.image_index]\n });\n }\n }\n\n const handleMouseDown = () => {\n setDragState(true);\n }\n\n const handleMouseUp = () => {\n setDragState(false);\n }\n\n useEffect(() => {\n if (threeSixtyRef.current && updateIndex >= 0 && updateIndex < imageArr.length) {\n threeSixtyRef.current.goto(updateIndex)\n }\n }, [updateIndex])\n\n useEffect(() => {\n document.addEventListener(`${containerName}_image_changed`, imageChange);\n if(viewerRef.current) {\n viewerRef.current.addEventListener('mousedown', handleMouseDown);\n viewerRef.current.addEventListener('mouseup', handleMouseUp);\n }\n return () => {\n document.removeEventListener(`${containerName}_image_changed`, imageChange);\n if(viewerRef.current) {\n viewerRef.current.removeEventListener('mousedown', handleMouseDown);\n viewerRef.current.removeEventListener('mouseup', handleMouseUp);\n }\n }\n }, [type]);\n\n useEffect(() => {\n if (threeSixtyRef.current) {\n let newImages = imageArr.map(ite => ite[imageKey])\n threeSixtyRef.current._updateImage(newImages);\n }\n }, [JSON.stringify(imageArr)])\n\n useEffect(() => {\n if (viewerRef && viewerRef.current) {\n if(loadedType.indexOf(type) === -1) {\n setAllImagesLoaded(false);\n }\n threeSixtyRef.current = new ThreeSixty(viewerRef.current, {\n image: imageArr.map(ite => ite[imageKey]),\n ...props\n });\n preloadImages(imageArr.map(ite => ite[imageKey]), () => {\n if (autoPlay) {\n threeSixtyRef.current.play();\n }\n setLoadedType([...loadedType, type])\n setAllImagesLoaded(true);\n threeSixtyRef.current._allowScroll();\n });\n return () => {\n if (viewerRef) {\n threeSixtyRef.current.destroy();\n }\n }\n }\n }, [type])\n\n return <>\n \n \n
\n
\n
\n
\n {\n !allImagesLoaded && (\n
\n
\n \n
\n
\n )\n }\n \n}\n\nexport default memo(ThreeSixtyViewer)\n","import React, { useState, useRef } from 'react'\nimport ThreeSixtyViewer from './Component/ThreeSixtyViewer';\n\nconst App = () => {\n const imageArr = [\n {\n \"id\": 408,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/Uo5MoH79Ss6GC99JEeWUyQ/raw/file.JPG\",\n \"order\": 0,\n \"angle\": 0.0\n },\n {\n \"id\": 409,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/dmGPc_YVRJior34RrOg5Jg/raw/file.JPG\",\n \"order\": 1,\n \"angle\": 0.0\n },\n {\n \"id\": 410,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/YswVlO9rRVKfYyJtb3WNAw/raw/file.JPG\",\n \"order\": 2,\n \"angle\": 0.0\n },\n {\n \"id\": 411,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/fkHmQtSGQim_qxkluSPZrg/raw/file.JPG\",\n \"order\": 3,\n \"angle\": 0.0\n },\n {\n \"id\": 412,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/zVChlMjHS7KA6TBqXB2G8w/raw/file.JPG\",\n \"order\": 4,\n \"angle\": 0.0\n },\n {\n \"id\": 413,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/ka46RbQBSrKZICjDeXEA0Q/raw/file.JPG\",\n \"order\": 5,\n \"angle\": 0.0\n },\n {\n \"id\": 414,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/QCra7_uRQGWeAGjF4mYS6w/raw/file.JPG\",\n \"order\": 6,\n \"angle\": 0.0\n },\n {\n \"id\": 415,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/AOyYcfhUSv6wuRLtHo3Ayw/raw/file.JPG\",\n \"order\": 7,\n \"angle\": 0.0\n },\n {\n \"id\": 416,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/I8dCYmSvQ_SqAWpQA5dFiQ/raw/file.JPG\",\n \"order\": 8,\n \"angle\": 0.0\n },\n {\n \"id\": 417,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/UirC4_wRRlu4w__VLeIHEw/raw/file.JPG\",\n \"order\": 9,\n \"angle\": 0.0\n },\n {\n \"id\": 418,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/_7P7LT5aQwCATllGvP4ilA/raw/file.JPG\",\n \"order\": 10,\n \"angle\": 0.0\n },\n {\n \"id\": 419,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/g8NdlqJyTUawVP%2BK9utmtA/raw/file.JPG\",\n \"order\": 11,\n \"angle\": 0.0\n },\n {\n \"id\": 420,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/hCASp9AETT2sZGc43SUMHA/raw/file.JPG\",\n \"order\": 12,\n \"angle\": 0.0\n },\n {\n \"id\": 421,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/4Yoh8TzDRdGLfsV6uXLy0A/raw/file.JPG\",\n \"order\": 13,\n \"angle\": 0.0\n },\n {\n \"id\": 422,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/67FkD5DOTja6oncBO%2BMBaQ/raw/file.JPG\",\n \"order\": 14,\n \"angle\": 0.0\n },\n {\n \"id\": 423,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/QDT5l6OLQbS1DvJIfVW%2BZQ/raw/file.JPG\",\n \"order\": 15,\n \"angle\": 0.0\n },\n {\n \"id\": 424,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/yz37V2yPSz6yOJtzcyblJg/raw/file.JPG\",\n \"order\": 16,\n \"angle\": 0.0\n },\n {\n \"id\": 425,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/btJmXmOlSSqaDY1rG3u9wQ/raw/file.JPG\",\n \"order\": 17,\n \"angle\": 0.0\n },\n {\n \"id\": 426,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/VXsDUl8wSa%2BBxxJFGtMoIw/raw/file.JPG\",\n \"order\": 18,\n \"angle\": 0.0\n },\n {\n \"id\": 427,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/aQLF04pjQvqUB84zRIPpJQ/raw/file.JPG\",\n \"order\": 19,\n \"angle\": 0.0\n },\n {\n \"id\": 428,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/%2BwqlvYPQQWWhkFfpAOVF4A/raw/file.JPG\",\n \"order\": 20,\n \"angle\": 0.0\n },\n {\n \"id\": 429,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/EybRuKdQSx68S5Mq7QsZ8w/raw/file.JPG\",\n \"order\": 21,\n \"angle\": 0.0\n },\n {\n \"id\": 430,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/HfQFLw_aSomR6sxmKWwB2g/raw/file.JPG\",\n \"order\": 22,\n \"angle\": 0.0\n },\n {\n \"id\": 431,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/smsdSxTTTzqXCrG6zZXe5w/raw/file.JPG\",\n \"order\": 23,\n \"angle\": 0.0\n },\n {\n \"id\": 432,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/MmbYIXdZRQGHvMi318pVcA/raw/file.JPG\",\n \"order\": 24,\n \"angle\": 0.0\n },\n {\n \"id\": 433,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/DxMWLFtDRDCkmHhelNonRQ/raw/file.JPG\",\n \"order\": 25,\n \"angle\": 0.0\n },\n {\n \"id\": 434,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/CVJgGUCYS4OBLZnvsk0JcQ/raw/file.JPG\",\n \"order\": 26,\n \"angle\": 0.0\n },\n {\n \"id\": 435,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/ZUodQrh8Qm29LC3h0lh9Ag/raw/file.JPG\",\n \"order\": 27,\n \"angle\": 0.0\n },\n {\n \"id\": 436,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/DJTQIDSDTbCO1QpCA2lo2Q/raw/file.JPG\",\n \"order\": 28,\n \"angle\": 0.0\n },\n {\n \"id\": 437,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/iIECfzMrRWaPLYoGxsv0eA/raw/file.JPG\",\n \"order\": 29,\n \"angle\": 0.0\n },\n {\n \"id\": 438,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/kEL%2Bk3ipQZyagR4sh_XxlA/raw/file.JPG\",\n \"order\": 30,\n \"angle\": 0.0\n },\n {\n \"id\": 439,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/urwB0yRHQOmbw_0XRtXH7Q/raw/file.JPG\",\n \"order\": 31,\n \"angle\": 0.0\n },\n {\n \"id\": 440,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/DvNYhFHZSGmL_bhQ%2B7Awug/raw/file.JPG\",\n \"order\": 32,\n \"angle\": 0.0\n },\n {\n \"id\": 441,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/mtNNYszkSX%2BZsz9R1c1xKw/raw/file.JPG\",\n \"order\": 33,\n \"angle\": 0.0\n },\n {\n \"id\": 442,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/wOgngAl2QyqBrQqzGgLIoQ/raw/file.JPG\",\n \"order\": 34,\n \"angle\": 0.0\n },\n {\n \"id\": 443,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/9KTg7LW5Q%2Ba%2BJFtWorXlgA/raw/file.JPG\",\n \"order\": 35,\n \"angle\": 0.0\n },\n {\n \"id\": 444,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/bRLQAgSDRUOG0z%2B7HUMfpQ/raw/file.JPG\",\n \"order\": 36,\n \"angle\": 0.0\n },\n {\n \"id\": 445,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/cYgHSj9MTdeLzukQpnzlCQ/raw/file.JPG\",\n \"order\": 37,\n \"angle\": 0.0\n },\n {\n \"id\": 446,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/e0mWUpAMTxKx9CSVVW53Ug/raw/file.JPG\",\n \"order\": 38,\n \"angle\": 0.0\n },\n {\n \"id\": 447,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/ilSLNm3VR_C2i22amtqmeA/raw/file.JPG\",\n \"order\": 39,\n \"angle\": 0.0\n },\n {\n \"id\": 448,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/C4sXOZGjTo27HmH99C_vPg/raw/file.JPG\",\n \"order\": 40,\n \"angle\": 0.0\n },\n {\n \"id\": 449,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/Cd4gMA_pT%2B2EUTwHbaMKPg/raw/file.JPG\",\n \"order\": 41,\n \"angle\": 0.0\n },\n {\n \"id\": 450,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/91Jzzq%2BSQeG9R4_UlZM8ow/raw/file.JPG\",\n \"order\": 42,\n \"angle\": 0.0\n },\n {\n \"id\": 451,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/z_HO%2BylrRnyFuUY4Og8iZg/raw/file.JPG\",\n \"order\": 43,\n \"angle\": 0.0\n },\n {\n \"id\": 452,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/xSNO6EeMR2SUuxr%2BbcYMlw/raw/file.JPG\",\n \"order\": 44,\n \"angle\": 0.0\n },\n {\n \"id\": 453,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/JZWMpvNESR299Mjfao6cHQ/raw/file.JPG\",\n \"order\": 45,\n \"angle\": 0.0\n },\n {\n \"id\": 454,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/xvhpFBHlRRyBaxf3x1yIzQ/raw/file.JPG\",\n \"order\": 46,\n \"angle\": 0.0\n },\n {\n \"id\": 455,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/chP%2BTjURRGqjs_18GAW5pw/raw/file.JPG\",\n \"order\": 47,\n \"angle\": 0.0\n },\n {\n \"id\": 456,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/Sn5dKUMOR_2h_nqekNbI3A/raw/file.JPG\",\n \"order\": 48,\n \"angle\": 0.0\n },\n {\n \"id\": 457,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/GfXXxZg7QwigDx0RIX2TBA/raw/file.JPG\",\n \"order\": 49,\n \"angle\": 0.0\n },\n {\n \"id\": 458,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/kQMovFzmRm2LeG5iF4UljA/raw/file.JPG\",\n \"order\": 50,\n \"angle\": 0.0\n },\n {\n \"id\": 459,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/1R1ZsxuYRP%2BU8gq60g17gg/raw/file.JPG\",\n \"order\": 51,\n \"angle\": 0.0\n },\n {\n \"id\": 460,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/oeFCGPZTS4GHwHqqny5g3Q/raw/file.JPG\",\n \"order\": 52,\n \"angle\": 0.0\n },\n {\n \"id\": 461,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/ud4C0n1aRW%2BvyXRX8wQ6aw/raw/file.JPG\",\n \"order\": 53,\n \"angle\": 0.0\n },\n {\n \"id\": 462,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/TfS66TWjRdGPsi_jnJCQPA/raw/file.JPG\",\n \"order\": 54,\n \"angle\": 0.0\n },\n {\n \"id\": 463,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/elFikWemTdWTKFIc6apXfg/raw/file.JPG\",\n \"order\": 55,\n \"angle\": 0.0\n },\n {\n \"id\": 464,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/X4LSxNmQS_aL5NMX63lyBw/raw/file.JPG\",\n \"order\": 56,\n \"angle\": 0.0\n },\n {\n \"id\": 465,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/O%2B83ISGwSgGUE0kr6NhUOg/raw/file.JPG\",\n \"order\": 57,\n \"angle\": 0.0\n },\n {\n \"id\": 466,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/Djm_EDU1SEaV0tLDZiUrOA/raw/file.JPG\",\n \"order\": 58,\n \"angle\": 0.0\n },\n {\n \"id\": 467,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/h8bPVoINQt%2BubW1qUhB%2B4g/raw/file.JPG\",\n \"order\": 59,\n \"angle\": 0.0\n },\n {\n \"id\": 468,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/P3gW5TsPS0mDBhSjPmJbNQ/raw/file.JPG\",\n \"order\": 60,\n \"angle\": 0.0\n },\n {\n \"id\": 469,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/EqyQB0I6RXO4ZXFWWQpNsQ/raw/file.JPG\",\n \"order\": 61,\n \"angle\": 0.0\n },\n {\n \"id\": 470,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/NvnQ7SS0QkieSWhzXfMzWQ/raw/file.JPG\",\n \"order\": 62,\n \"angle\": 0.0\n },\n {\n \"id\": 471,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/Pfzp%2BC_xS5yLxoqlyUcC5g/raw/file.JPG\",\n \"order\": 63,\n \"angle\": 0.0\n },\n {\n \"id\": 472,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/3SywP%2BveREekN5fEkbwEsA/raw/file.JPG\",\n \"order\": 64,\n \"angle\": 0.0\n },\n {\n \"id\": 473,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/GUgw3pE0THOUnn886OcrJw/raw/file.JPG\",\n \"order\": 65,\n \"angle\": 0.0\n },\n {\n \"id\": 474,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/NQkVWIFLQviaWlcdlIP5XQ/raw/file.JPG\",\n \"order\": 66,\n \"angle\": 0.0\n },\n {\n \"id\": 475,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/SH2cdDKzTVmZgldO72d12w/raw/file.JPG\",\n \"order\": 67,\n \"angle\": 0.0\n },\n {\n \"id\": 476,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/pPL5kEBBSSShZ0qzw0HcXw/raw/file.JPG\",\n \"order\": 68,\n \"angle\": 0.0\n },\n {\n \"id\": 477,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/BN7ZG5FXT1%2B%2BMmE%2BAPL8eQ/raw/file.JPG\",\n \"order\": 69,\n \"angle\": 0.0\n },\n {\n \"id\": 478,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/1bZQTfaCTcybjyVE0cCNfw/raw/file.JPG\",\n \"order\": 70,\n \"angle\": 0.0\n },\n {\n \"id\": 479,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/z9PKuevLTWq2VxL59uVSEA/raw/file.JPG\",\n \"order\": 71,\n \"angle\": 0.0\n },\n {\n \"id\": 480,\n \"uri\": \"//spinny-backend.s3.ap-south-1.amazonaws.com/img/efGKLe7jTB6Vt4cr133ZhA/raw/file.JPG\",\n \"order\": 72,\n \"angle\": 0.0\n }\n ]\n\n const [updateIndex, setUpdateIndex] = useState(0);\n // const inputRef = useRef(0);\n\n const handleImageChange = (image_index) => {\n console.log('image change', image_index)\n }\n\n // const handleInputChange = (value) => {\n // if (value && value > 0 && value < imageArr.length) {\n // setUpdateIndex(value)\n // }\n // }\n\n return (\n \n
\n {/* handleInputChange(inputRef.current.value)} /> */}\n \n
\n
\n )\n}\n\nexport default App","import React from 'react'\nimport ReactDOM from 'react-dom'\nimport App from './App'\n\nReactDOM.render(, document.getElementById('root'))\n"],"sourceRoot":""} -------------------------------------------------------------------------------- /examples/react/build/static/js/runtime-main.7bb332b4.js: -------------------------------------------------------------------------------- 1 | !function(e){function r(r){for(var n,l,a=r[0],f=r[1],i=r[2],p=0,s=[];p0.2%", 25 | "not dead", 26 | "not ie <= 11", 27 | "not op_mini all" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /examples/react/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashivliving/react-threesixty/f82461ef9b5bdc88c45744b7a57b837d87ecf0cb/examples/react/public/favicon.ico -------------------------------------------------------------------------------- /examples/react/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | React Ui 13 | 14 | 15 | 16 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/react/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "react-threesixty", 3 | "name": "react-threesixty", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } -------------------------------------------------------------------------------- /examples/react/src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashivliving/react-threesixty/f82461ef9b5bdc88c45744b7a57b837d87ecf0cb/examples/react/src/.DS_Store -------------------------------------------------------------------------------- /examples/react/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from 'react' 2 | import ThreeSixtyViewer from './Component/ThreeSixtyViewer'; 3 | import { data } from './constants'; 4 | 5 | const App = () => { 6 | const [updateIndex, setUpdateIndex] = useState(0); 7 | return ( 8 | 9 |
10 | {/* handleInputChange(inputRef.current.value)} /> */} 11 | {data.map((it, index) => index !== 3 && 12 |
13 | 27 |
28 | )} 29 |
30 |
31 | ) 32 | } 33 | 34 | export default App -------------------------------------------------------------------------------- /examples/react/src/Component/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashivliving/react-threesixty/f82461ef9b5bdc88c45744b7a57b837d87ecf0cb/examples/react/src/Component/.DS_Store -------------------------------------------------------------------------------- /examples/react/src/Component/ThreeSixtyHotspots.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const style = { 4 | wrapper: (hotspot, isRenderUI) => ({ 5 | position: 'absolute', 6 | zIndex: 1, 7 | top: `${hotspot.top * 100}%`, 8 | left: `${hotspot.left * 100}%`, 9 | display: hotspot.display ? 'block' : 'none', 10 | transform: 'translate(-50%, -50%)', 11 | ...Object.assign({}, isRenderUI ? {} : { 12 | content: '', 13 | width: '20px', 14 | height: '20px', 15 | borderRadius: '50%', 16 | backgroundColor: 'blue' 17 | }) 18 | }) 19 | } 20 | 21 | /** 22 | * 23 | * @param { Array<{ left: string; top: string; dentAngle: string; display: boolean; ui: * }> } hotspots 24 | * @param {*} renderUI - UI we want to render 25 | * @param { Function } clickHandler - click handler for hotspot 26 | */ 27 | const ThreeSixtyHotspots = ({ hotspots, clickHandler, renderUI }) => { 28 | return hotspots.map((hotspot, index) => ( 29 |
clickHandler && clickHandler(hotspot)}>{renderUI ? renderUI(hotspot) : <>}
30 | )) 31 | } 32 | 33 | export default ThreeSixtyHotspots; -------------------------------------------------------------------------------- /examples/react/src/Component/ThreeSixtyViewer.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, memo, useState } from 'react'; 2 | import ThreeSixty from '@ashivliving/threesixty-js'; 3 | import updateHotspots from '../utils/updateHotspots'; 4 | import ThreeSixtyHotspots from './ThreeSixtyHotspots'; 5 | import ZoomPan from './ZoomPan'; 6 | 7 | const styles = { 8 | threeSixtyWrap: (allImagesLoaded) => ({ 9 | display: allImagesLoaded ? 'block' : 'none', 10 | width: '100%', 11 | height: '100%', 12 | position: 'relative' 13 | }), 14 | transformComponent: (allImagesLoaded, dragState, isMobile, noMargin) => ({ 15 | width: 'fit-content', 16 | height: isMobile && !noMargin ? 'fit-content' : '100%', 17 | visibility: allImagesLoaded ? 'visible' : 'hidden', 18 | position: 'relative', 19 | transform: 'translateX(-50%)', 20 | maxWidth: '100%', 21 | maxHeight: '100%', 22 | left: '50%', 23 | overflow: 'hidden', 24 | cursor: `url(${dragState ? 'https://spinny-images.s3.ap-south-1.amazonaws.com/static-asset/icons/drag_cursor.svg' : 'https://spinny-images.s3.ap-south-1.amazonaws.com/static-asset/icons/cursor.svg'}), auto`, 25 | ...Object.assign({}, isMobile && !noMargin ? { margin: 'auto' } : {}) 26 | }), 27 | viewer: (allImagesLoaded, dragState) => ({ 28 | width: '100%', 29 | height: '100%', 30 | display: 'flex', 31 | cursor: `url(${dragState ? 'https://spinny-images.s3.ap-south-1.amazonaws.com/static-asset/icons/drag_cursor.svg' : 'https://spinny-images.s3.ap-south-1.amazonaws.com/static-asset/icons/cursor.svg'}), auto`, 32 | visibility: allImagesLoaded ? 'visible' : 'hidden', 33 | }), 34 | zoomOption: { 35 | position: 'absolute', 36 | bottom: '1em', 37 | right: '1em', 38 | width: '2em', 39 | zIndex: '2' 40 | }, 41 | zoomButton: { padding: '0px', width: '1.5em', height: '1.5em', fontSize: '20px', fontWeight: '500', cursor: 'pointer' }, 42 | loadingImgWrap: (isMobile, imageArr, startIndex, imageKey) => ({ 43 | display: 'block', 44 | position: 'relative', 45 | width: '100%', 46 | height: '100%', 47 | left: '0px', 48 | top: '0px', 49 | backgroundPosition: 'center', 50 | backgroundSize: 'contain', 51 | backgroundRepeat: 'no-repeat', 52 | backgroundImage: `url(${imageArr[startIndex][imageKey]})`, 53 | cursor: 'not-allowed' 54 | }), 55 | loading: { 56 | position: 'absolute', 57 | width: '100px', 58 | height: '100px', 59 | top: 'calc(50% - 50px)', 60 | left: 'calc(50% - 50px)' 61 | } 62 | } 63 | 64 | const ThreeSixtyViewer = (props) => { 65 | const { isMobile = false, imageKey = 'image_url', zoomImageKey = 'zoom_image_url', type = 'exterior', 66 | autoPlay, startIndex = 0, updateIndex, handleImageChange, handleZoomInOut, showZoomOption = false, 67 | containerName = 'reactThreesixtyContainer', hotspotClickHandler, renderHotspotUI, renderExtraElement, 68 | noOuterMargin, imageArr, threeSixtyWrapStyle, zoomButtonStyle } = props; 69 | const viewerRef = useRef(null); 70 | const threeSixtyRef = useRef(null); 71 | const [dragState, setDragState] = useState(false); 72 | const [loadedType, setLoadedType] = useState([]); 73 | const [allImagesLoaded, setAllImagesLoaded] = useState(false); 74 | const [isZoomIn, setIsZoomIn] = useState(false); 75 | const [hotspots, sethotspots] = useState(props.hotspots); 76 | 77 | const preloadImages = (urls, allImagesLoadedCallback) => { 78 | var loadedCounter = 0; 79 | var toBeLoadedNumber = urls.length; 80 | urls.forEach(function (url) { 81 | preloadImage(url, function () { 82 | loadedCounter++; 83 | if (loadedCounter === toBeLoadedNumber) { 84 | allImagesLoadedCallback(); 85 | } 86 | }); 87 | }); 88 | function preloadImage(url, anImageLoadedCallback) { 89 | var img = new Image(); 90 | img.onload = anImageLoadedCallback; 91 | img.src = url; 92 | } 93 | } 94 | 95 | const imageChange = (e = null) => { 96 | const imgAngle = imageArr[e ? e.detail.image_index : startIndex].angle; 97 | 98 | props.hotspots?.length && sethotspots((old) => updateHotspots(old, imgAngle)); 99 | if (e && e.detail && handleImageChange) { 100 | handleImageChange({ 101 | index: e.detail.image_index, 102 | item: imageArr[e.detail.image_index] 103 | }); 104 | } 105 | } 106 | 107 | const handleMouseDown = () => { 108 | setDragState(true); 109 | } 110 | 111 | const handleMouseUp = () => { 112 | setDragState(false); 113 | } 114 | 115 | const updateZoomImage = (imageIndex) => { 116 | let zoomImageUrl = imageArr[imageIndex][zoomImageKey] ? imageArr[imageIndex][zoomImageKey] : imageArr[imageIndex][imageKey]; 117 | if (imageArr[imageIndex][zoomImageKey]) { 118 | preloadImages([zoomImageUrl], () => { 119 | let newImages = imageArr.map((ite, index) => index === imageIndex ? zoomImageUrl : ite[imageKey]) 120 | threeSixtyRef.current._updateImage(newImages); 121 | }) 122 | } 123 | } 124 | 125 | const handleZoomChange = (transformWrapperData) => { 126 | if (transformWrapperData.scale <= 1) { 127 | if (isZoomIn) { 128 | setIsZoomIn(false); 129 | if (handleZoomInOut) { 130 | handleZoomInOut(false); 131 | } 132 | threeSixtyRef.current._allowScroll(); 133 | } 134 | } else { 135 | if (!isZoomIn) { 136 | setIsZoomIn(true); 137 | if (handleZoomInOut) { 138 | handleZoomInOut(true); 139 | } 140 | threeSixtyRef.current._stopScroll(); 141 | updateZoomImage(threeSixtyRef.current.index); 142 | } 143 | } 144 | } 145 | 146 | const handleZoomAction = (type, scale) => { 147 | if (type === 'zoom-in') { 148 | if (!isZoomIn) { 149 | updateZoomImage(threeSixtyRef.current.index); 150 | if (handleZoomInOut) { 151 | handleZoomInOut(true); 152 | } 153 | setIsZoomIn(true); 154 | threeSixtyRef.current._stopScroll(); 155 | } 156 | } else if (type === 'zoom-out') { 157 | if (scale < 1.5) { 158 | if (isZoomIn) { 159 | if (handleZoomInOut) { 160 | handleZoomInOut(false); 161 | } 162 | setIsZoomIn(false); 163 | threeSixtyRef.current._allowScroll(); 164 | } 165 | } 166 | } else { 167 | if (isZoomIn) { 168 | if (handleZoomInOut) { 169 | handleZoomInOut(false); 170 | } 171 | setIsZoomIn(false); 172 | threeSixtyRef.current._allowScroll(); 173 | } 174 | } 175 | } 176 | 177 | useEffect(() => { 178 | if (threeSixtyRef.current && updateIndex >= 0 && updateIndex < imageArr.length) { 179 | threeSixtyRef.current.goto(updateIndex) 180 | } 181 | }, [updateIndex]) 182 | 183 | useEffect(() => { 184 | document.addEventListener(`${containerName}_image_changed`, imageChange); 185 | if (viewerRef.current) { 186 | viewerRef.current.addEventListener('mousedown', handleMouseDown); 187 | viewerRef.current.addEventListener('mouseup', handleMouseUp); 188 | } 189 | return () => { 190 | document.removeEventListener(`${containerName}_image_changed`, imageChange); 191 | if (viewerRef.current) { 192 | viewerRef.current.removeEventListener('mousedown', handleMouseDown); 193 | viewerRef.current.removeEventListener('mouseup', handleMouseUp); 194 | } 195 | } 196 | }, [type]); 197 | 198 | useEffect(() => { 199 | if (threeSixtyRef.current) { 200 | let newImages = imageArr.map(ite => ite[imageKey]) 201 | threeSixtyRef.current._updateImage(newImages); 202 | } 203 | props.hotspots?.length && imageChange(); 204 | }, [JSON.stringify(imageArr)]) 205 | 206 | useEffect(() => { 207 | if (viewerRef && viewerRef.current) { 208 | if (loadedType.indexOf(type) === -1) { 209 | setAllImagesLoaded(false); 210 | } 211 | threeSixtyRef.current = new ThreeSixty(viewerRef.current, { 212 | image: imageArr.map(ite => ite[imageKey]), 213 | ...props 214 | }); 215 | preloadImages(imageArr.map(ite => ite[imageKey]), () => { 216 | if (autoPlay) { 217 | threeSixtyRef.current.play(); 218 | } 219 | setLoadedType([...loadedType, type]) 220 | setAllImagesLoaded(true); 221 | threeSixtyRef.current._allowScroll(); 222 | }); 223 | return () => { 224 | if (viewerRef) { 225 | threeSixtyRef.current.destroy(); 226 | } 227 | } 228 | } 229 | }, [type]) 230 | 231 | const renderThreesixty = () => { 232 | if (isMobile) { 233 | return ( 234 |
235 | 236 |
237 |
238 | {renderExtraElement &&
{renderExtraElement()}
} 239 |
240 | {hotspots?.length > 0 && } 241 |
242 |
243 |
244 | ) 245 | } else { 246 | return ( 247 |
248 | 249 |
250 |
251 | {renderExtraElement &&
{renderExtraElement()}
} 252 |
253 | {hotspots?.length > 0 && } 254 |
255 |
256 |
257 | ) 258 | } 259 | } 260 | 261 | return <> 262 | { renderThreesixty()} 263 | { 264 | !allImagesLoaded && ( 265 |
266 |
267 | 271 |
272 |
273 | ) 274 | } 275 | 276 | } 277 | 278 | export default memo(ThreeSixtyViewer) -------------------------------------------------------------------------------- /examples/react/src/Component/ZoomPan.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch"; 3 | 4 | 5 | const ZoomPan = ({ handleZoomChange, isDesktop, isZoomIn, children, handleZoomAction, showZoomOption, styles, zoomButtonStyle }) => { 6 | if (isDesktop) { 7 | return ( 8 | 19 | {({ zoomIn, zoomOut, resetTransform, scale }) => ( 20 | <> 21 | 22 | {children} 23 | 24 | { 25 | showZoomOption && ( 26 |
27 | 31 | 36 | 41 |
42 | ) 43 | } 44 | 45 | )} 46 |
47 | ) 48 | } 49 | 50 | return ( 51 | handleZoomChange(e)} 61 | > 62 | 63 | {children} 64 | 65 | 66 | ) 67 | } 68 | export default ZoomPan -------------------------------------------------------------------------------- /examples/react/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | 5 | ReactDOM.render(, document.getElementById('root')) 6 | -------------------------------------------------------------------------------- /examples/react/src/utils/updateHotspots.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @param { Array<{ left: string; top: string; dentAngle: string; display: boolean; }> } hotspotsData 4 | * @param {number} imgAngle - angle of current img 5 | * @param {{ x: number, y: number }} center - x, y cordinates of center 6 | * 7 | */ 8 | const updateHotspots = (hotspotsData, imgAngle, center = { x: 0.5, y: 0.5 }) => { 9 | const curAngle = imgAngle > 180 ? imgAngle - 360 : imgAngle; 10 | 11 | // position wise 360 angle 12 | const getAngle = (pos) => { 13 | switch (pos) { 14 | case 'front': 15 | return 0; 16 | case 'left': 17 | return 90; 18 | case 'right': 19 | return -90; 20 | case 'back': 21 | return 180; 22 | default: 23 | return 0; 24 | } 25 | } 26 | 27 | // for hotspot to display in this range 28 | const getMaxAngle = (pos) => { 29 | return 30; 30 | } 31 | 32 | const getCoordinatesFromPos = (pos, r) => { 33 | switch (pos) { 34 | case 'front': 35 | return { x: center.x, y: center.y - r } 36 | case 'left': 37 | return { x: center.x - r, y: center.y } 38 | case 'right': 39 | return { x: center.x + r, y: center.y } 40 | case 'back': 41 | return { x: center.x, y: r + center.y } 42 | case 'upper': 43 | return { x: center.x, y: center.y - r } 44 | default: 45 | return { x: center.x, y: center.y - r } 46 | } 47 | } 48 | 49 | const rotate = (cx, cy, x, y, angle) => { 50 | const radians = (Math.PI / 180) * angle, 51 | cos = Math.cos(radians), 52 | sin = Math.sin(radians), 53 | nx = (cos * (x - cx)) + (sin * (y - cy)) + cx, 54 | ny = (cos * (y - cy)) - (sin * (x - cx)) + cy; 55 | return { x: Number(nx.toFixed(5)), y: Number(ny.toFixed(5)) }; 56 | } 57 | 58 | const getDeviation = (currX, currY, curAngle) => { 59 | const { x, y } = rotate(center.x, center.y, currX, currY, curAngle); 60 | return { x, y } 61 | } 62 | 63 | return hotspotsData.map((hotspot) => { 64 | let minAngle, maxAngle, isDiplay, dentAngle = hotspot.dentAngle; 65 | minAngle = getAngle(hotspot.position) - getMaxAngle(hotspot.position); 66 | maxAngle = getAngle(hotspot.position) + getMaxAngle(hotspot.position); 67 | 68 | if (imgAngle > 180 && hotspot.position === 'back') { 69 | isDiplay = imgAngle >= minAngle && imgAngle <= maxAngle; 70 | } else if (hotspot.position === 'upper') { 71 | if (dentAngle <= 30 && dentAngle >= -30) { 72 | isDiplay = curAngle <= 30 && curAngle >= -30; 73 | } else if (dentAngle >= 60 && dentAngle <= 120) { 74 | isDiplay = curAngle >= 60 && curAngle <= 120; 75 | } else if (dentAngle <= -60 && dentAngle >= -120) { 76 | isDiplay = curAngle <= -60 && curAngle >= -120; 77 | } else { 78 | isDiplay = curAngle >= 150 || curAngle <= -150; 79 | } 80 | } else { 81 | isDiplay = curAngle >= minAngle && curAngle <= maxAngle; 82 | } 83 | 84 | const cor = getCoordinatesFromPos(hotspot.position, hotspot.radius); 85 | const deviation = getDeviation(cor.x, cor.y, -(curAngle + hotspot.dentAngle)); 86 | 87 | return { 88 | ...hotspot, 89 | left: deviation.x, 90 | display: isDiplay 91 | }; 92 | }) 93 | } 94 | 95 | export default updateHotspots; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-threesixty", 3 | "version": "2.0.8", 4 | "description": "A Simple React 360 Library using threesixty.js", 5 | "keywords": [ 6 | "React 360", 7 | "react-360", 8 | "360" 9 | ], 10 | "main": "dist/index.js", 11 | "module": "dist/index.modern.js", 12 | "umd:main": "dist/index.umd.js", 13 | "scripts": { 14 | "prebuild": "rimraf dist", 15 | "build": "rollup -c", 16 | "watch": "rollup -c --watch", 17 | "dev": "concurrently \" npm run watch \" \" npm run start --prefix examples/react \" ", 18 | "test": "run-s test:unit test:lint test:build", 19 | "test:lint": "eslint src/**/*.js", 20 | "test:unit": "cross-env CI=1 react-scripts test --env=jsdom", 21 | "test:watch": "react-scripts test --env=jsdom --coverage --collectCoverageFrom=src/components/**/*.js", 22 | "test:build": "run-s build", 23 | "lint:fix": "prettier-eslint '**/*.js' --write", 24 | "prepublish": "npm run build", 25 | "publish": "npm run build && npm publish" 26 | }, 27 | "author": "", 28 | "license": "MIT", 29 | "husky": { 30 | "hooks": { 31 | "pre-commit": "lint-staged" 32 | } 33 | }, 34 | "lint-staged": { 35 | "src/**/*.{js,jsx}": [ 36 | "npm run lint:fix" 37 | ] 38 | }, 39 | "devDependencies": { 40 | "@babel/core": "^7.10.3", 41 | "@babel/preset-env": "^7.10.3", 42 | "@babel/preset-react": "^7.10.1", 43 | "@testing-library/jest-dom": "^5.10.1", 44 | "@testing-library/react": "^10.4.1", 45 | "@testing-library/user-event": "^12.0.7", 46 | "babel-eslint": "^10.0.3", 47 | "babel-loader": "^8.1.0", 48 | "babel-plugin-macros": "^2.8.0", 49 | "babel-plugin-styled-components": "^1.10.7", 50 | "concurrently": "^5.2.0", 51 | "cross-env": "^7.0.2", 52 | "eslint": "^6.8.0", 53 | "eslint-config-prettier": "^6.7.0", 54 | "eslint-config-standard": "^14.1.0", 55 | "eslint-config-standard-react": "^9.2.0", 56 | "eslint-plugin-import": "^2.18.2", 57 | "eslint-plugin-node": "^11.0.0", 58 | "eslint-plugin-prettier": "^3.1.1", 59 | "eslint-plugin-promise": "^4.2.1", 60 | "eslint-plugin-react": "^7.20.0", 61 | "eslint-plugin-standard": "^4.0.1", 62 | "husky": "^4.2.5", 63 | "lint-staged": "^10.2.11", 64 | "npm-run-all": "^4.1.5", 65 | "prettier": "^2.0.5", 66 | "prettier-eslint-cli": "^5.0.0", 67 | "prop-types": "^15.7.2", 68 | "react": "^16.13.1", 69 | "react-dom": "^16.13.1", 70 | "react-scripts": "^3.4.1", 71 | "react-test-renderer": "^16.13.1", 72 | "rimraf": "^3.0.2", 73 | "rollup": "^2.18.0", 74 | "rollup-plugin-babel": "^4.4.0", 75 | "rollup-plugin-commonjs": "^10.1.0", 76 | "rollup-plugin-node-resolve": "^5.2.0", 77 | "rollup-plugin-peer-deps-external": "^2.2.2", 78 | "rollup-plugin-terser": "^6.1.0", 79 | "rollup-plugin-uglify": "^6.0.4", 80 | "styled-components": "^5.1.1" 81 | }, 82 | "peerDependencies": { 83 | "react": "^16.13.1", 84 | "prop-types": "^15.7.2", 85 | "styled-components": "^5.1.1" 86 | }, 87 | "files": [ 88 | "dist" 89 | ], 90 | "repository": { 91 | "type": "git", 92 | "url": "https://github.com/ashivliving/react-threesixty" 93 | }, 94 | "dependencies": { 95 | "@ashivliving/threesixty-js": "^3.0.6", 96 | "react-zoom-pan-pinch": "^1.6.1" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from "rollup-plugin-babel"; 2 | import commonjs from "rollup-plugin-commonjs"; 3 | import resolve from "rollup-plugin-node-resolve"; 4 | import external from "rollup-plugin-peer-deps-external"; 5 | import { terser } from "rollup-plugin-terser"; 6 | import { uglify } from "rollup-plugin-uglify"; 7 | 8 | const input = 'src/index.js'; 9 | const output = 'dist/index'; 10 | 11 | export default [ 12 | { 13 | input: input, 14 | output: { 15 | file: `${output}.js`, 16 | format: 'cjs' 17 | }, 18 | plugins: [ 19 | resolve({ 20 | browser: true 21 | }), 22 | commonjs({ 23 | include: [ 24 | 'node_modules/**' 25 | ], 26 | namedExports: { 27 | "react-dom": ["createPortal"], 28 | }, 29 | }), 30 | babel({ 31 | exclude: "node_modules/**" 32 | }), 33 | external(), 34 | uglify() 35 | ], 36 | }, 37 | { 38 | input: input, 39 | output: { 40 | file: `${output}.modern.js`, 41 | format: 'es' 42 | }, 43 | 44 | plugins: [ 45 | resolve(), 46 | commonjs({ 47 | include: [ 48 | 'node_modules/**' 49 | ], 50 | namedExports: { 51 | "react-dom": ["createPortal"], 52 | }, 53 | }), 54 | babel({ 55 | exclude: "node_modules/**", 56 | }), 57 | external(), 58 | terser(), 59 | ], 60 | }, 61 | { 62 | input: input, 63 | output: { 64 | name: 'ReactThreeSixty', 65 | file: `${output}.umd.js`, 66 | globals: { 67 | react: 'React', 68 | 'styled-components': 'styled', 69 | 'prop-types': 'PropTypes', 70 | 'prop-types/checkPropTypes': 'checkPropTypes' 71 | }, 72 | format: 'umd' 73 | }, 74 | plugins: [ 75 | resolve(), 76 | commonjs({ 77 | include: [ 78 | 'node_modules/**' 79 | ], 80 | namedExports: { 81 | "react-dom": ["createPortal"], 82 | }, 83 | }), 84 | external(), 85 | babel({ 86 | exclude: "node_modules/**", 87 | 88 | }), 89 | terser(), 90 | ], 91 | } 92 | ] -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashivliving/react-threesixty/f82461ef9b5bdc88c45744b7a57b837d87ecf0cb/src/.DS_Store -------------------------------------------------------------------------------- /src/components/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashivliving/react-threesixty/f82461ef9b5bdc88c45744b7a57b837d87ecf0cb/src/components/.DS_Store -------------------------------------------------------------------------------- /src/components/ThreeSixtyHotspots.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const style = { 4 | wrapper: (hotspot, isRenderUI) => ({ 5 | position: 'absolute', 6 | zIndex: 1, 7 | top: `${hotspot.top * 100}%`, 8 | left: `${hotspot.left * 100}%`, 9 | display: hotspot.display ? 'block' : 'none', 10 | transform: 'translate(-50%, -50%)', 11 | ...Object.assign({}, isRenderUI ? {} : { 12 | content: '', 13 | width: '20px', 14 | height: '20px', 15 | borderRadius: '50%', 16 | backgroundColor: 'blue' 17 | }) 18 | }) 19 | } 20 | 21 | /** 22 | * 23 | * @param { Array<{ left: string; top: string; dentAngle: string; display: boolean; ui: * }> } hotspots 24 | * @param {*} renderUI - UI we want to render 25 | * @param { Function } clickHandler - click handler for hotspot 26 | */ 27 | const ThreeSixtyHotspots = ({ hotspots, clickHandler, renderUI }) => { 28 | return hotspots.map((hotspot, index) => ( 29 |
clickHandler && clickHandler(hotspot)}>{renderUI ? renderUI(hotspot) : <>}
30 | )) 31 | } 32 | 33 | export default ThreeSixtyHotspots; -------------------------------------------------------------------------------- /src/components/ThreeSixtyViewer.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, memo, useState } from 'react'; 2 | import ThreeSixty from '@ashivliving/threesixty-js'; 3 | import updateHotspots from '../utils/updateHotspots'; 4 | import ThreeSixtyHotspots from './ThreeSixtyHotspots'; 5 | import ZoomPan from './ZoomPan'; 6 | 7 | const styles = { 8 | threeSixtyWrap: (allImagesLoaded) => ({ 9 | display: allImagesLoaded ? 'block' : 'none', 10 | width: '100%', 11 | height: '100%', 12 | position: 'relative' 13 | }), 14 | transformComponent: (allImagesLoaded, dragState, isMobile, noMargin) => ({ 15 | width: 'fit-content', 16 | height: isMobile && !noMargin ? 'fit-content' : '100%', 17 | visibility: allImagesLoaded ? 'visible' : 'hidden', 18 | position: 'relative', 19 | transform: 'translateX(-50%)', 20 | maxWidth: '100%', 21 | maxHeight: '100%', 22 | left: '50%', 23 | overflow: 'hidden', 24 | cursor: `url(${dragState ? 'https://spinny-images.s3.ap-south-1.amazonaws.com/static-asset/icons/drag_cursor.svg' : 'https://spinny-images.s3.ap-south-1.amazonaws.com/static-asset/icons/cursor.svg'}), auto`, 25 | ...Object.assign({}, isMobile && !noMargin ? { margin: 'auto' } : {}) 26 | }), 27 | viewer: (allImagesLoaded, dragState) => ({ 28 | width: '100%', 29 | height: '100%', 30 | display: 'flex', 31 | cursor: `url(${dragState ? 'https://spinny-images.s3.ap-south-1.amazonaws.com/static-asset/icons/drag_cursor.svg' : 'https://spinny-images.s3.ap-south-1.amazonaws.com/static-asset/icons/cursor.svg'}), auto`, 32 | visibility: allImagesLoaded ? 'visible' : 'hidden', 33 | }), 34 | zoomOption: { 35 | position: 'absolute', 36 | bottom: '1em', 37 | right: '1em', 38 | width: '2em', 39 | zIndex: '2' 40 | }, 41 | zoomButton: { padding: '0px', width: '1.5em', height: '1.5em', fontSize: '20px', fontWeight: '500', cursor: 'pointer' }, 42 | loadingImgWrap: (isMobile, imageArr, startIndex, imageKey) => ({ 43 | display: 'block', 44 | position: 'relative', 45 | width: '100%', 46 | height: '100%', 47 | left: '0px', 48 | top: '0px', 49 | backgroundPosition: 'center', 50 | backgroundSize: 'contain', 51 | backgroundRepeat: 'no-repeat', 52 | backgroundImage: `url(${imageArr[startIndex][imageKey]})`, 53 | cursor: 'not-allowed' 54 | }), 55 | loading: { 56 | position: 'absolute', 57 | width: '100px', 58 | height: '100px', 59 | top: 'calc(50% - 50px)', 60 | left: 'calc(50% - 50px)' 61 | } 62 | } 63 | 64 | const ThreeSixtyViewer = (props) => { 65 | const { isMobile = false, imageKey = 'image_url', zoomImageKey = 'zoom_image_url', type = 'exterior', 66 | autoPlay, startIndex = 0, updateIndex, handleImageChange, handleZoomInOut, showZoomOption = false, 67 | containerName = 'reactThreesixtyContainer', hotspotClickHandler, renderHotspotUI, renderExtraElement, 68 | noOuterMargin, imageArr, threeSixtyWrapStyle, zoomButtonStyle } = props; 69 | const viewerRef = useRef(null); 70 | const threeSixtyRef = useRef(null); 71 | const [dragState, setDragState] = useState(false); 72 | const [loadedType, setLoadedType] = useState([]); 73 | const [allImagesLoaded, setAllImagesLoaded] = useState(false); 74 | const [isZoomIn, setIsZoomIn] = useState(false); 75 | const [hotspots, sethotspots] = useState(props.hotspots); 76 | 77 | const preloadImages = (urls, allImagesLoadedCallback) => { 78 | var loadedCounter = 0; 79 | var toBeLoadedNumber = urls.length; 80 | urls.forEach(function (url) { 81 | preloadImage(url, function () { 82 | loadedCounter++; 83 | if (loadedCounter === toBeLoadedNumber) { 84 | allImagesLoadedCallback(); 85 | } 86 | }); 87 | }); 88 | function preloadImage(url, anImageLoadedCallback) { 89 | var img = new Image(); 90 | img.onload = anImageLoadedCallback; 91 | img.src = url; 92 | } 93 | } 94 | 95 | const imageChange = (e = null) => { 96 | const imgAngle = imageArr[e ? e.detail.image_index : startIndex].angle; 97 | 98 | props.hotspots?.length && sethotspots((old) => updateHotspots(old, imgAngle)); 99 | if (e && e.detail && handleImageChange) { 100 | handleImageChange({ 101 | index: e.detail.image_index, 102 | item: imageArr[e.detail.image_index] 103 | }); 104 | } 105 | } 106 | 107 | const handleMouseDown = () => { 108 | setDragState(true); 109 | } 110 | 111 | const handleMouseUp = () => { 112 | setDragState(false); 113 | } 114 | 115 | const updateZoomImage = (imageIndex) => { 116 | let zoomImageUrl = imageArr[imageIndex][zoomImageKey] ? imageArr[imageIndex][zoomImageKey] : imageArr[imageIndex][imageKey]; 117 | if (imageArr[imageIndex][zoomImageKey]) { 118 | preloadImages([zoomImageUrl], () => { 119 | let newImages = imageArr.map((ite, index) => index === imageIndex ? zoomImageUrl : ite[imageKey]) 120 | threeSixtyRef.current._updateImage(newImages); 121 | }) 122 | } 123 | } 124 | 125 | const handleZoomChange = (transformWrapperData) => { 126 | if (transformWrapperData.scale <= 1) { 127 | if (isZoomIn) { 128 | setIsZoomIn(false); 129 | if (handleZoomInOut) { 130 | handleZoomInOut(false); 131 | } 132 | threeSixtyRef.current._allowScroll(); 133 | } 134 | } else { 135 | if (!isZoomIn) { 136 | setIsZoomIn(true); 137 | if (handleZoomInOut) { 138 | handleZoomInOut(true); 139 | } 140 | threeSixtyRef.current._stopScroll(); 141 | updateZoomImage(threeSixtyRef.current.index); 142 | } 143 | } 144 | } 145 | 146 | const handleZoomAction = (type, scale) => { 147 | if (type === 'zoom-in') { 148 | if (!isZoomIn) { 149 | updateZoomImage(threeSixtyRef.current.index); 150 | if (handleZoomInOut) { 151 | handleZoomInOut(true); 152 | } 153 | setIsZoomIn(true); 154 | threeSixtyRef.current._stopScroll(); 155 | } 156 | } else if (type === 'zoom-out') { 157 | if (scale < 1.5) { 158 | if (isZoomIn) { 159 | if (handleZoomInOut) { 160 | handleZoomInOut(false); 161 | } 162 | setIsZoomIn(false); 163 | threeSixtyRef.current._allowScroll(); 164 | } 165 | } 166 | } else { 167 | if (isZoomIn) { 168 | if (handleZoomInOut) { 169 | handleZoomInOut(false); 170 | } 171 | setIsZoomIn(false); 172 | threeSixtyRef.current._allowScroll(); 173 | } 174 | } 175 | } 176 | 177 | useEffect(() => { 178 | if (threeSixtyRef.current && updateIndex >= 0 && updateIndex < imageArr.length) { 179 | threeSixtyRef.current.goto(updateIndex) 180 | } 181 | }, [updateIndex]) 182 | 183 | useEffect(() => { 184 | document.addEventListener(`${containerName}_image_changed`, imageChange); 185 | if (viewerRef.current) { 186 | viewerRef.current.addEventListener('mousedown', handleMouseDown); 187 | viewerRef.current.addEventListener('mouseup', handleMouseUp); 188 | } 189 | return () => { 190 | document.removeEventListener(`${containerName}_image_changed`, imageChange); 191 | if (viewerRef.current) { 192 | viewerRef.current.removeEventListener('mousedown', handleMouseDown); 193 | viewerRef.current.removeEventListener('mouseup', handleMouseUp); 194 | } 195 | } 196 | }, [type]); 197 | 198 | useEffect(() => { 199 | if (threeSixtyRef.current) { 200 | let newImages = imageArr.map(ite => ite[imageKey]) 201 | threeSixtyRef.current._updateImage(newImages); 202 | } 203 | props.hotspots?.length && imageChange(); 204 | }, [JSON.stringify(imageArr)]) 205 | 206 | useEffect(() => { 207 | if (viewerRef && viewerRef.current) { 208 | if (loadedType.indexOf(type) === -1) { 209 | setAllImagesLoaded(false); 210 | } 211 | threeSixtyRef.current = new ThreeSixty(viewerRef.current, { 212 | image: imageArr.map(ite => ite[imageKey]), 213 | ...props 214 | }); 215 | preloadImages(imageArr.map(ite => ite[imageKey]), () => { 216 | if (autoPlay) { 217 | threeSixtyRef.current.play(); 218 | } 219 | setLoadedType([...loadedType, type]) 220 | setAllImagesLoaded(true); 221 | threeSixtyRef.current._allowScroll(); 222 | }); 223 | return () => { 224 | if (viewerRef) { 225 | threeSixtyRef.current.destroy(); 226 | } 227 | } 228 | } 229 | }, [type]) 230 | 231 | const renderThreesixty = () => { 232 | if (isMobile) { 233 | return ( 234 |
235 | 236 |
237 |
238 | {renderExtraElement &&
{renderExtraElement()}
} 239 |
240 | {hotspots?.length > 0 && } 241 |
242 |
243 |
244 | ) 245 | } else { 246 | return ( 247 |
248 | 249 |
250 |
251 | {renderExtraElement &&
{renderExtraElement()}
} 252 |
253 | {hotspots?.length > 0 && } 254 |
255 |
256 |
257 | ) 258 | } 259 | } 260 | 261 | return <> 262 | { renderThreesixty()} 263 | { 264 | !allImagesLoaded && ( 265 |
266 |
267 | 271 |
272 |
273 | ) 274 | } 275 | 276 | } 277 | 278 | export default memo(ThreeSixtyViewer) -------------------------------------------------------------------------------- /src/components/ZoomPan.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch"; 3 | 4 | 5 | const ZoomPan = ({ handleZoomChange, isDesktop, isZoomIn, children, handleZoomAction, showZoomOption, styles, zoomButtonStyle }) => { 6 | if (isDesktop) { 7 | return ( 8 | 19 | {({ zoomIn, zoomOut, resetTransform, scale }) => ( 20 | <> 21 | 22 | {children} 23 | 24 | { 25 | showZoomOption && ( 26 |
27 | 31 | 36 | 41 |
42 | ) 43 | } 44 | 45 | )} 46 |
47 | ) 48 | } 49 | 50 | return ( 51 | handleZoomChange(e)} 61 | > 62 | 63 | {children} 64 | 65 | 66 | ) 67 | } 68 | export default ZoomPan -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import ThreeSixtyViewer from './components/ThreeSixtyViewer' 2 | import ZoomPan from './components/ZoomPan'; 3 | export { ThreeSixtyViewer, ZoomPan } 4 | -------------------------------------------------------------------------------- /src/utils/updateHotspots.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @param { Array<{ left: string; top: string; dentAngle: string; display: boolean; }> } hotspotsData 4 | * @param {number} imgAngle - angle of current img 5 | * @param {{ x: number, y: number }} center - x, y cordinates of center 6 | * 7 | */ 8 | const updateHotspots = (hotspotsData, imgAngle, center = { x: 0.5, y: 0.5 }) => { 9 | const curAngle = imgAngle > 180 ? imgAngle - 360 : imgAngle; 10 | 11 | // position wise 360 angle 12 | const getAngle = (pos) => { 13 | switch (pos) { 14 | case 'front': 15 | return 0; 16 | case 'left': 17 | return 90; 18 | case 'right': 19 | return -90; 20 | case 'back': 21 | return 180; 22 | default: 23 | return 0; 24 | } 25 | } 26 | 27 | // for hotspot to display in this range 28 | const getMaxAngle = (pos) => { 29 | return 45; 30 | } 31 | 32 | const getCoordinatesFromPos = (pos, r) => { 33 | switch (pos) { 34 | case 'front': 35 | return { x: center.x, y: center.y - r } 36 | case 'left': 37 | return { x: center.x - r, y: center.y } 38 | case 'right': 39 | return { x: center.x + r, y: center.y } 40 | case 'back': 41 | return { x: center.x, y: r + center.y } 42 | case 'upper': 43 | return { x: center.x, y: center.y - r } 44 | default: 45 | return { x: center.x, y: center.y - r } 46 | } 47 | } 48 | 49 | const rotate = (cx, cy, x, y, angle) => { 50 | const radians = (Math.PI / 180) * angle, 51 | cos = Math.cos(radians), 52 | sin = Math.sin(radians), 53 | nx = (cos * (x - cx)) + (sin * (y - cy)) + cx, 54 | ny = (cos * (y - cy)) - (sin * (x - cx)) + cy; 55 | return { x: Number(nx.toFixed(5)), y: Number(ny.toFixed(5)) }; 56 | } 57 | 58 | const getDeviation = (currX, currY, curAngle) => { 59 | const { x, y } = rotate(center.x, center.y, currX, currY, curAngle); 60 | return { x, y } 61 | } 62 | 63 | return hotspotsData.map((hotspot) => { 64 | let minAngle, maxAngle, isDiplay; 65 | minAngle = getAngle(hotspot.position) - getMaxAngle(hotspot.position); 66 | maxAngle = getAngle(hotspot.position) + getMaxAngle(hotspot.position); 67 | 68 | if (imgAngle > 180 && hotspot.position === 'back') { 69 | isDiplay = imgAngle > minAngle && imgAngle < maxAngle; 70 | } else { 71 | isDiplay = curAngle > minAngle && curAngle < maxAngle; 72 | } 73 | 74 | const cor = getCoordinatesFromPos(hotspot.position, hotspot.radius); 75 | const deviation = getDeviation(cor.x, cor.y, -(curAngle + hotspot.dentAngle)); 76 | 77 | return { 78 | ...hotspot, 79 | left: deviation.x, 80 | display: isDiplay || hotspot.position === 'upper' 81 | }; 82 | }) 83 | } 84 | 85 | export default updateHotspots; --------------------------------------------------------------------------------