├── .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;
--------------------------------------------------------------------------------