├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .postcssrc.js ├── .prettierrc ├── .vscode └── tasks.json ├── LICENSE ├── README.md ├── build ├── build.js ├── check-versions.js ├── dev-client.js ├── dev-server.js ├── utils.js ├── vue-loader.conf.js ├── webpack.base.conf.js ├── webpack.dev.conf.js ├── webpack.prod.conf.js ├── webpack.release.js └── webpack.release.min.js ├── config ├── dev.env.js ├── index.js └── prod.env.js ├── demos ├── App.vue ├── assets │ ├── .gitkeep │ ├── ClashofClans.mp4 │ ├── equirectangular.jpg │ ├── faces │ │ ├── pano_b.jpg │ │ ├── pano_d.jpg │ │ ├── pano_f.jpg │ │ ├── pano_l.jpg │ │ ├── pano_r.jpg │ │ └── pano_u.jpg │ └── pano.png ├── components │ └── demo-block.vue ├── index.html ├── index.js ├── pages │ ├── demo-cube-pano.vue │ ├── demo-pano.vue │ ├── demo-scene.vue │ ├── demo-tour.vue │ ├── demo-video-pano.vue │ └── index.js └── router │ └── index.js ├── dist ├── vue-vr.js └── vue-vr.min.js ├── package.json └── src ├── Pano.vue ├── Scene.vue ├── Tour.vue ├── assets └── hotspot.png ├── controls └── OrbitControls.js ├── index.js ├── lib ├── hotspots.js └── panolens.js └── util.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { "modules": false }], 4 | "stage-2" 5 | ], 6 | "plugins": ["transform-runtime"], 7 | "comments": false, 8 | "env": { 9 | "test": { 10 | "presets": ["env", "stage-2"], 11 | "plugins": [ "istanbul" ] 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /config/ 3 | /dist/ 4 | /docs/ 5 | /*.js 6 | /test/unit/coverage/ 7 | /src/libs 8 | /src/lib 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: ['plugin:vue/essential', '@vue/airbnb'], 7 | parserOptions: { 8 | parser: 'babel-eslint' 9 | }, 10 | globals: { 11 | window: true, 12 | THREE: true 13 | }, 14 | plugins: ['prettier'], 15 | rules: { 16 | 'vue/experimental-script-setup-vars': 'off', 17 | 'operator-linebreak': 'off', 18 | 'arrow-parens': 'off', 19 | 'comma-dangle': [ 20 | 'error', 21 | { 22 | arrays: 'never', 23 | objects: 'never', 24 | imports: 'never', 25 | exports: 'never', 26 | functions: 'ignore' 27 | } 28 | ], 29 | 'prefer-destructuring': [ 30 | 'error', 31 | { 32 | array: true, 33 | object: false 34 | }, 35 | { 36 | enforceForRenamedProperties: false 37 | } 38 | ], 39 | 'arrow-parens': 'off', 40 | 'function-paren-newline': 'off', 41 | 'import/no-unresolved': 'off', 42 | 'import/extensions': 'off', 43 | 'import/no-extraneous-dependencies': 'off', 44 | // 'import/no-extraneous-dependencies': context => [ 45 | // 'error', 46 | // { 47 | // devDependencies: true, 48 | // packageDir: [context.getFilename(), __dirname] 49 | // } 50 | // ], 51 | 'vue/no-unused-components': [ 52 | 'error', 53 | { 54 | ignoreWhenBindingPresent: true 55 | } 56 | ], 57 | 'vue/no-use-v-if-with-v-for': 'off', 58 | 'no-console': 'off', 59 | 'vue/no-parsing-error': [ 60 | 2, 61 | { 62 | 'x-invalid-end-tag': false, 63 | 'control-character-in-input-stream': false 64 | } 65 | ], 66 | 'vue/no-use-v-if-with-v-for': [ 67 | 'error', 68 | { 69 | allowUsingIterationVar: true // default: false 70 | } 71 | ], 72 | 'max-len': [ 73 | 'error', 74 | { 75 | code: 100, 76 | ignorePattern: true, 77 | ignoreUrls: true, 78 | ignoreStrings: true 79 | } 80 | ], 81 | 'no-param-reassign': [ 82 | 2, 83 | { 84 | props: false 85 | } 86 | ] 87 | } 88 | }; 89 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | docs/ 7 | dist/demos/ 8 | .idea/ 9 | package-lock.json 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | docs/ 7 | demos/ 8 | dist/assets/ 9 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | module.exports = { 3 | "plugins": { 4 | // to edit target browsers: use "browserlist" field in package.json 5 | "autoprefixer": {} 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "printWidth": 80, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "trailingComma": "none" 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "eslint", 8 | "problemMatcher": [ 9 | "$eslint-stylish" 10 | ] 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Muhridin Ibragimov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | License 4 | 5 | 6 | npm version 7 | 8 | 9 | HitCount 10 | 11 | 12 | size 13 | 14 |

15 | 16 | # Vue VR 17 | A Wrapper of [Panolens](https://pchen66.github.io/Panolens/) for building VR applications with Vue 18 | based on [threejs](https://threejs.org/) 19 | 20 | ## Demos 21 | [Image Pano](https://mudin.github.io/vue-vr/#/demo-pano) 22 | 23 | [Cube Pano](https://mudin.github.io/vue-vr/#/demo-cube-pano) 24 | 25 | [Video Pano](https://mudin.github.io/vue-vr/#/demo-video-pano) 26 | 27 | ![360 Video Demo](https://mudin.github.io/vue-vr/assets/360video.gif?raw=true) 28 | 29 | [VR Tour](https://mudin.github.io/vue-vr/#/demo-tour) 30 | 31 | ![VR Tour](https://mudin.github.io/vue-vr/assets/vrtour.gif?raw=true) 32 | 33 | ## Getting started 34 | using npm 35 | ``` 36 | npm install vuejs-vr --save 37 | ``` 38 | Or using script tag for global use 39 | ```html 40 | 41 | ``` 42 | 43 | Or Download vue-vr.min.js and include it in your html 44 | 45 | ## Installing & Running Locally 46 | 47 | Clone the repository using git: 48 | ``` 49 | git clone https://github.com/mudin/vue-vr.git 50 | ``` 51 | Installing all dependencies: 52 | ``` 53 | npm install 54 | ``` 55 | Build by webpack: 56 | ``` 57 | npm run-script build 58 | ``` 59 | Run locally: 60 | ``` 61 | npm start 62 | ``` 63 | This will start development server on localhost:8080 64 | 65 | ## Usage 66 | 67 | ####For simple panorama: 68 | Panorama by equirectangular image 69 | ```vue 70 | 73 | 80 | ``` 81 | Or 82 | ```vue 83 | 84 | 85 | 86 | 87 | 88 | 89 |
90 | 91 |
92 | 93 | 94 | 99 | 100 | ``` 101 | 102 | 103 | ####For cube faces: 104 | Panorama with a six-face cubemap 105 | ```vue 106 | 109 | 116 | ``` 117 | Note: `%s` replaced by `'l'|'f'|'r'|'b'|'u'|'d'` 118 | Or 119 | ```vue 120 | 121 | 122 | 123 | 124 | 125 | 126 |
127 | 128 |
129 | 130 | 131 | 136 | 137 | ``` 138 | 139 | 140 | 141 | ####360 video: 142 | Panorama with 360 video 143 | ```vue 144 | 147 | 154 | ``` 155 | Or 156 | ```vue 157 | 158 | 159 | 160 | 161 | 162 | 163 |
164 | 165 |
166 | 167 | 168 | 173 | 174 | ``` 175 | 176 | ## TODO List 177 | * Hotspots 178 | * Multi touch on touchsceen devices 179 | * 3D objects 180 | * HlS, Live Streaming video Support 181 | 182 | ## Contributing 183 | If you would like to contribute code you can do so through GitHub by forking the repository and sending a pull request. 184 | 185 | ## LICENSE 186 | MIT 187 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')() 2 | 3 | process.env.NODE_ENV = 'production' 4 | 5 | var ora = require('ora') 6 | var rm = require('rimraf') 7 | var path = require('path') 8 | var chalk = require('chalk') 9 | var webpack = require('webpack') 10 | var config = require('../config') 11 | var webpackConfig = require('./webpack.prod.conf') 12 | 13 | var spinner = ora('building for production...') 14 | spinner.start() 15 | 16 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 17 | if (err) throw err 18 | webpack(webpackConfig, function (err, stats) { 19 | spinner.stop() 20 | if (err) throw err 21 | process.stdout.write(stats.toString({ 22 | colors: true, 23 | modules: false, 24 | children: false, 25 | chunks: false, 26 | chunkModules: false 27 | }) + '\n\n') 28 | 29 | console.log(chalk.cyan(' Build complete.\n')) 30 | console.log(chalk.yellow( 31 | ' Tip: built files are meant to be served over an HTTP server.\n' + 32 | ' Opening index.html over file:// won\'t work.\n' 33 | )) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /build/check-versions.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk') 2 | var semver = require('semver') 3 | var packageConfig = require('../package.json') 4 | var shell = require('shelljs') 5 | function exec (cmd) { 6 | return require('child_process').execSync(cmd).toString().trim() 7 | } 8 | 9 | var versionRequirements = [ 10 | { 11 | name: 'node', 12 | currentVersion: semver.clean(process.version), 13 | versionRequirement: packageConfig.engines.node 14 | }, 15 | ] 16 | 17 | if (shell.which('npm')) { 18 | versionRequirements.push({ 19 | name: 'npm', 20 | currentVersion: exec('npm --version'), 21 | versionRequirement: packageConfig.engines.npm 22 | }) 23 | } 24 | 25 | module.exports = function () { 26 | var warnings = [] 27 | for (var i = 0; i < versionRequirements.length; i++) { 28 | var mod = versionRequirements[i] 29 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 30 | warnings.push(mod.name + ': ' + 31 | chalk.red(mod.currentVersion) + ' should be ' + 32 | chalk.green(mod.versionRequirement) 33 | ) 34 | } 35 | } 36 | 37 | if (warnings.length) { 38 | console.log('') 39 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 40 | console.log() 41 | for (var i = 0; i < warnings.length; i++) { 42 | var warning = warnings[i] 43 | console.log(' ' + warning) 44 | } 45 | console.log() 46 | process.exit(1) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /build/dev-client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | require('eventsource-polyfill') 3 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 4 | 5 | hotClient.subscribe(function (event) { 6 | if (event.action === 'reload') { 7 | window.location.reload() 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /build/dev-server.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')() 2 | 3 | const config = require('../config') 4 | 5 | if (!process.env.NODE_ENV) { 6 | process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV) 7 | } 8 | 9 | const opn = require('opn') 10 | const path = require('path') 11 | const express = require('express') 12 | const webpack = require('webpack') 13 | const proxyMiddleware = require('http-proxy-middleware') 14 | const webpackConfig = require('./webpack.dev.conf') 15 | 16 | // default port where dev server listens for incoming traffic 17 | const port = process.env.PORT || config.dev.port 18 | // if 8080 port is bound 19 | 20 | // automatically open browser, if not set will be false 21 | const autoOpenBrowser = !!config.dev.autoOpenBrowser 22 | // Define HTTP proxies to your custom API backend 23 | // https://github.com/chimurai/http-proxy-middleware 24 | const proxyTable = config.dev.proxyTable 25 | 26 | const app = express() 27 | const compiler = webpack(webpackConfig) 28 | 29 | const devMiddleware = require('webpack-dev-middleware')(compiler, { 30 | publicPath: webpackConfig.output.publicPath, 31 | quiet: true 32 | }) 33 | 34 | const hotMiddleware = require('webpack-hot-middleware')(compiler, { 35 | log: () => {} 36 | }) 37 | // force page reload when html-webpack-plugin template changes 38 | compiler.plugin('compilation', function (compilation) { 39 | compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 40 | hotMiddleware.publish({ action: 'reload' }) 41 | cb() 42 | }) 43 | }) 44 | 45 | // proxy api requests 46 | Object.keys(proxyTable).forEach(function (context) { 47 | var options = proxyTable[context] 48 | if (typeof options === 'string') { 49 | options = { target: options } 50 | } 51 | app.use(proxyMiddleware(options.filter || context, options)) 52 | }) 53 | 54 | // handle fallback for HTML5 history API 55 | app.use(require('connect-history-api-fallback')()) 56 | 57 | // serve webpack bundle output 58 | app.use(devMiddleware) 59 | 60 | // enable hot-reload and state-preserving 61 | // compilation error display 62 | app.use(hotMiddleware) 63 | 64 | // serve pure static assets 65 | const staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) 66 | app.use(staticPath, express.static('./demos/assets')) 67 | 68 | const uri = 'http://localhost:' + port 69 | 70 | let _resolve 71 | const readyPromise = new Promise(resolve => { 72 | _resolve = resolve 73 | }) 74 | 75 | console.log('> Starting dev server...') 76 | devMiddleware.waitUntilValid(() => { 77 | console.log('> Listening at ' + uri + '\n') 78 | // when env is testing, don't need open it 79 | if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') { 80 | opn(uri) 81 | } 82 | _resolve() 83 | }) 84 | 85 | const server = app.listen(port) 86 | 87 | module.exports = { 88 | ready: readyPromise, 89 | close: () => { 90 | server.close() 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /build/utils.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const config = require('../config') 3 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | 5 | exports.assetsPath = function (_path) { 6 | var assetsSubDirectory = process.env.NODE_ENV === 'production' 7 | ? config.build.assetsSubDirectory 8 | : config.dev.assetsSubDirectory 9 | return path.posix.join(assetsSubDirectory, _path) 10 | } 11 | 12 | exports.cssLoaders = function (options) { 13 | options = options || {} 14 | 15 | var cssLoader = { 16 | loader: 'css-loader', 17 | options: { 18 | minimize: process.env.NODE_ENV === 'production', 19 | sourceMap: options.sourceMap 20 | } 21 | } 22 | 23 | // generate loader string to be used with extract text plugin 24 | function generateLoaders (loader, loaderOptions) { 25 | var loaders = [cssLoader] 26 | if (loader) { 27 | loaders.push({ 28 | loader: loader + '-loader', 29 | options: Object.assign({}, loaderOptions, { 30 | sourceMap: options.sourceMap 31 | }) 32 | }) 33 | } 34 | 35 | // Extract CSS when that option is specified 36 | // (which is the case during production build) 37 | if (options.extract) { 38 | return ExtractTextPlugin.extract({ 39 | use: loaders, 40 | fallback: 'vue-style-loader' 41 | }) 42 | } else { 43 | return ['vue-style-loader'].concat(loaders) 44 | } 45 | } 46 | 47 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 48 | return { 49 | css: generateLoaders(), 50 | postcss: generateLoaders(), 51 | less: generateLoaders('less'), 52 | sass: generateLoaders('sass', { indentedSyntax: true }), 53 | scss: generateLoaders('sass'), 54 | stylus: generateLoaders('stylus'), 55 | styl: generateLoaders('stylus') 56 | } 57 | } 58 | 59 | // Generate loaders for standalone style files (outside of .vue) 60 | exports.styleLoaders = function (options) { 61 | var output = [] 62 | var loaders = exports.cssLoaders(options) 63 | for (var extension in loaders) { 64 | var loader = loaders[extension] 65 | output.push({ 66 | test: new RegExp('\\.' + extension + '$'), 67 | use: loader 68 | }) 69 | } 70 | return output 71 | } -------------------------------------------------------------------------------- /build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | var config = require('../config') 3 | var isProduction = process.env.NODE_ENV === 'production' 4 | 5 | module.exports = { 6 | loaders: utils.cssLoaders() 7 | } 8 | -------------------------------------------------------------------------------- /build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var utils = require('./utils') 3 | var config = require('../config') 4 | var vueLoaderConfig = require('./vue-loader.conf') 5 | const webpack = require('webpack'); 6 | 7 | 8 | function resolve (dir) { 9 | return path.join(__dirname, '..', dir) 10 | } 11 | 12 | module.exports = { 13 | entry: { 14 | app: './demos/index.js' 15 | }, 16 | output: { 17 | path: config.build.assetsRoot, 18 | filename: '[name].js', 19 | publicPath: process.env.NODE_ENV === 'production' 20 | ? config.build.assetsPublicPath 21 | : config.dev.assetsPublicPath 22 | }, 23 | resolve: { 24 | extensions: ['.js', '.vue', '.json'], 25 | alias: { 26 | '@': resolve('src') 27 | } 28 | }, 29 | module: { 30 | rules: [ 31 | { 32 | test: /\.vue$/, 33 | loader: 'vue-loader', 34 | options: vueLoaderConfig 35 | }, 36 | { 37 | test: /\.js$/, 38 | loader: 'babel-loader', 39 | include: [resolve('src'), resolve('test'), resolve('demo'),] 40 | }, 41 | { 42 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 43 | loader: 'url-loader', 44 | options: { 45 | limit: 10000, 46 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 47 | } 48 | }, 49 | { 50 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 51 | loader: 'url-loader', 52 | options: { 53 | limit: 10000, 54 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 55 | } 56 | } 57 | ] 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | var webpack = require('webpack') 3 | var config = require('../config') 4 | var merge = require('webpack-merge') 5 | var baseWebpackConfig = require('./webpack.base.conf') 6 | var HtmlWebpackPlugin = require('html-webpack-plugin') 7 | var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 8 | 9 | // add hot-reload related code to entry chunks 10 | Object.keys(baseWebpackConfig.entry).forEach(function (name) { 11 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name]) 12 | }) 13 | 14 | module.exports = merge(baseWebpackConfig, { 15 | module: { 16 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }) 17 | }, 18 | // cheap-module-eval-source-map is faster for development 19 | devtool: '#cheap-module-eval-source-map', 20 | plugins: [ 21 | new webpack.DefinePlugin({ 22 | 'process.env': config.dev.env 23 | }), 24 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 25 | new webpack.HotModuleReplacementPlugin(), 26 | new webpack.NoEmitOnErrorsPlugin(), 27 | // https://github.com/ampedandwired/html-webpack-plugin 28 | new HtmlWebpackPlugin({ 29 | filename: 'index.html', 30 | template: './demos/index.html', 31 | inject: true 32 | }), 33 | new FriendlyErrorsPlugin() 34 | ] 35 | }) 36 | -------------------------------------------------------------------------------- /build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var utils = require('./utils') 3 | var webpack = require('webpack') 4 | var config = require('../config') 5 | var merge = require('webpack-merge') 6 | var baseWebpackConfig = require('./webpack.base.conf') 7 | var CopyWebpackPlugin = require('copy-webpack-plugin') 8 | var HtmlWebpackPlugin = require('html-webpack-plugin') 9 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 10 | var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 11 | 12 | var env = config.build.env 13 | 14 | var webpackConfig = merge(baseWebpackConfig, { 15 | module: { 16 | rules: utils.styleLoaders({ 17 | sourceMap: config.build.productionSourceMap, 18 | extract: true 19 | }) 20 | }, 21 | devtool: config.build.productionSourceMap ? '#source-map' : false, 22 | output: { 23 | path: config.build.assetsRoot, 24 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 25 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 26 | }, 27 | plugins: [ 28 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 29 | new webpack.DefinePlugin({ 30 | 'process.env': env 31 | }), 32 | new webpack.optimize.UglifyJsPlugin({ 33 | compress: { 34 | warnings: false 35 | }, 36 | sourceMap: true 37 | }), 38 | // extract css into its own file 39 | new ExtractTextPlugin({ 40 | filename: utils.assetsPath('css/[name].[contenthash].css') 41 | }), 42 | // Compress extracted CSS. We are using this plugin so that possible 43 | // duplicated CSS from different components can be deduped. 44 | new OptimizeCSSPlugin({ 45 | cssProcessorOptions: { 46 | safe: true 47 | } 48 | }), 49 | // generate dist index.html with correct asset hash for caching. 50 | // you can customize output by editing /index.html 51 | // see https://github.com/ampedandwired/html-webpack-plugin 52 | new HtmlWebpackPlugin({ 53 | filename: config.build.index, 54 | template: 'demos/index.html', 55 | inject: true, 56 | minify: { 57 | removeComments: true, 58 | collapseWhitespace: true, 59 | removeAttributeQuotes: true 60 | // more options: 61 | // https://github.com/kangax/html-minifier#options-quick-reference 62 | }, 63 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 64 | chunksSortMode: 'dependency' 65 | }), 66 | // split vendor js into its own file 67 | new webpack.optimize.CommonsChunkPlugin({ 68 | name: 'vendor', 69 | minChunks: function (module, count) { 70 | // any required modules inside node_modules are extracted to vendor 71 | return ( 72 | module.resource && 73 | /\.js$/.test(module.resource) && 74 | module.resource.indexOf( 75 | path.join(__dirname, '../node_modules') 76 | ) === 0 77 | ) 78 | } 79 | }), 80 | // extract webpack runtime and module manifest to its own file in order to 81 | // prevent vendor hash from being updated whenever app bundle is updated 82 | new webpack.optimize.CommonsChunkPlugin({ 83 | name: 'manifest', 84 | chunks: ['vendor'] 85 | }), 86 | // copy custom assets assets 87 | new CopyWebpackPlugin([ 88 | { 89 | from: path.resolve(__dirname, '../demos/assets'), 90 | to: config.build.assetsSubDirectory, 91 | ignore: ['.*'] 92 | } 93 | ]) 94 | ] 95 | }) 96 | 97 | if (config.build.productionGzip) { 98 | var CompressionWebpackPlugin = require('compression-webpack-plugin') 99 | 100 | webpackConfig.plugins.push( 101 | new CompressionWebpackPlugin({ 102 | asset: '[path].gz[query]', 103 | algorithm: 'gzip', 104 | test: new RegExp( 105 | '\\.(' + 106 | config.build.productionGzipExtensions.join('|') + 107 | ')$' 108 | ), 109 | threshold: 10240, 110 | minRatio: 0.8 111 | }) 112 | ) 113 | } 114 | 115 | if (config.build.bundleAnalyzerReport) { 116 | var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 117 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 118 | } 119 | 120 | module.exports = webpackConfig 121 | -------------------------------------------------------------------------------- /build/webpack.release.js: -------------------------------------------------------------------------------- 1 | var vue = require('vue-loader') 2 | var path = require('path') 3 | var config = require('../config') 4 | var utils = require('./utils') 5 | var webpack = require("webpack") 6 | var ExtractTextPlugin = require("extract-text-webpack-plugin") 7 | var projectRoot = path.resolve(__dirname, '../') 8 | var baseWebpackConfig = require('./webpack.base.conf') 9 | var vueLoaderConfig = require('./vue-loader.conf') 10 | 11 | function resolve (dir) { 12 | return path.join(__dirname, '..', dir) 13 | } 14 | 15 | module.exports = { 16 | entry: { 17 | 'vue-vr': './src/index.js' 18 | }, 19 | externals: { 20 | vue: { 21 | root: 'Vue', 22 | commonjs: 'vue', 23 | commonjs2: 'vue', 24 | amd: 'vue' 25 | } 26 | }, 27 | output: { 28 | path: config.release.assetsRoot, 29 | filename: '[name].js', 30 | library: 'VueVR', 31 | libraryTarget: 'umd' 32 | }, 33 | module: { 34 | rules: [ 35 | { 36 | test: /\.vue$/, 37 | loader: 'vue-loader', 38 | options: vueLoaderConfig 39 | }, 40 | { 41 | test: /\.js$/, 42 | loader: 'babel-loader', 43 | include: [resolve('src')] 44 | } 45 | ] 46 | } 47 | } 48 | 49 | if (process.env.NODE_ENV === 'production') { 50 | 51 | delete module.exports.devtool 52 | module.exports.plugins = [ 53 | new webpack.DefinePlugin({ 54 | 'process.env': { 55 | NODE_ENV: '"production"' 56 | } 57 | }), 58 | new webpack.optimize.UglifyJsPlugin({ 59 | compress: { 60 | warnings: false 61 | } 62 | }), 63 | new ExtractTextPlugin({ 64 | filename: utils.assetsPath('css/[name].[contenthash].css') 65 | }), 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /build/webpack.release.min.js: -------------------------------------------------------------------------------- 1 | var config = require('./webpack.release.js') 2 | var webpack = require('webpack') 3 | var utils = require('./utils') 4 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | 6 | config.output.filename = config.output.filename.replace(/\.js$/, '.min.js') 7 | 8 | delete config.devtool 9 | 10 | config.plugins = [ 11 | new webpack.optimize.UglifyJsPlugin({ 12 | sourceMap: false, 13 | compress: { 14 | warnings: false 15 | } 16 | }), 17 | new ExtractTextPlugin({ 18 | filename: utils.assetsPath('css/[name].[contenthash].css') 19 | }), 20 | ] 21 | 22 | module.exports = config 23 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var prodEnv = require('./prod.env') 3 | 4 | module.exports = merge(prodEnv, { 5 | NODE_ENV: '"development"' 6 | }) 7 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | // see http://vuejs-templates.github.io/webpack for documentation. 2 | var path = require('path') 3 | 4 | module.exports = { 5 | build: { 6 | env: require('./prod.env'), 7 | index: path.resolve(__dirname, '../docs/index.html'), 8 | assetsRoot: path.resolve(__dirname, '../docs'), 9 | assetsSubDirectory: 'assets', 10 | assetsPublicPath: './', 11 | productionSourceMap: true, 12 | // Gzip off by default as many popular static hosts such as 13 | // Surge or Netlify already gzip all static assets for you. 14 | // Before setting to `true`, make sure to: 15 | // npm install --save-dev compression-webpack-plugin 16 | productionGzip: false, 17 | productionGzipExtensions: ['js', 'css'], 18 | // Run the build command with an extra argument to 19 | // View the bundle analyzer report after build finishes: 20 | // `npm run build --report` 21 | // Set to `true` or `false` to always turn it on or off 22 | bundleAnalyzerReport: process.env.npm_config_report 23 | }, 24 | release: { 25 | env: require('./prod.env'), 26 | assetsRoot: path.resolve(__dirname, '../dist') 27 | }, 28 | dev: { 29 | env: require('./dev.env'), 30 | port: 8080, 31 | autoOpenBrowser: true, 32 | assetsSubDirectory: 'assets', 33 | assetsPublicPath: '/', 34 | proxyTable: {}, 35 | // CSS Sourcemaps off by default because relative paths are "buggy" 36 | // with this option, according to the CSS-Loader README 37 | // (https://github.com/webpack/css-loader#sourcemaps) 38 | // In our experience, they generally work as expected, 39 | // just be aware of this issue when enabling this option. 40 | cssSourceMap: false 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NODE_ENV: '"production"' 3 | } 4 | -------------------------------------------------------------------------------- /demos/App.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 37 | 38 | 185 | -------------------------------------------------------------------------------- /demos/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mudin/vue-vr/883761af5a24951bd6dc3928066d5813453106f9/demos/assets/.gitkeep -------------------------------------------------------------------------------- /demos/assets/ClashofClans.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mudin/vue-vr/883761af5a24951bd6dc3928066d5813453106f9/demos/assets/ClashofClans.mp4 -------------------------------------------------------------------------------- /demos/assets/equirectangular.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mudin/vue-vr/883761af5a24951bd6dc3928066d5813453106f9/demos/assets/equirectangular.jpg -------------------------------------------------------------------------------- /demos/assets/faces/pano_b.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mudin/vue-vr/883761af5a24951bd6dc3928066d5813453106f9/demos/assets/faces/pano_b.jpg -------------------------------------------------------------------------------- /demos/assets/faces/pano_d.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mudin/vue-vr/883761af5a24951bd6dc3928066d5813453106f9/demos/assets/faces/pano_d.jpg -------------------------------------------------------------------------------- /demos/assets/faces/pano_f.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mudin/vue-vr/883761af5a24951bd6dc3928066d5813453106f9/demos/assets/faces/pano_f.jpg -------------------------------------------------------------------------------- /demos/assets/faces/pano_l.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mudin/vue-vr/883761af5a24951bd6dc3928066d5813453106f9/demos/assets/faces/pano_l.jpg -------------------------------------------------------------------------------- /demos/assets/faces/pano_r.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mudin/vue-vr/883761af5a24951bd6dc3928066d5813453106f9/demos/assets/faces/pano_r.jpg -------------------------------------------------------------------------------- /demos/assets/faces/pano_u.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mudin/vue-vr/883761af5a24951bd6dc3928066d5813453106f9/demos/assets/faces/pano_u.jpg -------------------------------------------------------------------------------- /demos/assets/pano.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mudin/vue-vr/883761af5a24951bd6dc3928066d5813453106f9/demos/assets/pano.png -------------------------------------------------------------------------------- /demos/components/demo-block.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 74 | 75 | 148 | -------------------------------------------------------------------------------- /demos/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | vue-vr 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /demos/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | 5 | new Vue({ // eslint-disable-line no-new 6 | el: '#app', 7 | router, 8 | render: h => h(App) 9 | }) 10 | -------------------------------------------------------------------------------- /demos/pages/demo-cube-pano.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 79 | -------------------------------------------------------------------------------- /demos/pages/demo-pano.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 99 | 100 | 112 | -------------------------------------------------------------------------------- /demos/pages/demo-scene.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 140 | -------------------------------------------------------------------------------- /demos/pages/demo-tour.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 231 | -------------------------------------------------------------------------------- /demos/pages/demo-video-pano.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 79 | -------------------------------------------------------------------------------- /demos/pages/index.js: -------------------------------------------------------------------------------- 1 | import DemoPano from './demo-pano' 2 | import DemoCubePano from './demo-cube-pano' 3 | import DemoVideoPano from './demo-video-pano' 4 | import DemoScene from './demo-scene' 5 | import DemoTour from './demo-tour' 6 | 7 | const pages = [ 8 | DemoPano, 9 | DemoCubePano, 10 | DemoVideoPano, 11 | DemoScene, 12 | DemoTour 13 | ] 14 | 15 | export { pages } 16 | -------------------------------------------------------------------------------- /demos/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | import { pages } from '../pages' 5 | 6 | Vue.use(Router) 7 | 8 | const routes = pages.map(page => { 9 | const name = page.name 10 | return { 11 | path: '/' + name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(), 12 | name, 13 | component: page 14 | } 15 | }) 16 | 17 | console.log(routes) 18 | 19 | routes.push({ 20 | path: '*', 21 | redirect: '/demo-pano' 22 | }) 23 | 24 | export default new Router({ routes }) 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuejs-vr", 3 | "description": "A framework for building VR applications with Vue", 4 | "keywords": [ 5 | "vue", 6 | "vue-vr", 7 | "panolens.js", 8 | "three.js", 9 | "component", 10 | "vr", 11 | "vr tour" 12 | ], 13 | "version": "1.2.2", 14 | "author": "mudin ", 15 | "main": "dist/vue-vr.min.js", 16 | "scripts": { 17 | "dev": "node build/dev-server.js", 18 | "start": "node build/dev-server.js", 19 | "build": "node build/build.js", 20 | "release": "webpack --progress --hide-modules --config ./build/webpack.release.js && cross-env NODE_ENV=production webpack --progress --hide-modules --config ./build/webpack.release.min.js" 21 | }, 22 | "dependencies": { 23 | "three": "^0.125.0", 24 | "vue": "^2.2.6" 25 | }, 26 | "devDependencies": { 27 | "@vue/cli-plugin-babel": "^4.5.8", 28 | "@vue/cli-plugin-eslint": "^4.5.8", 29 | "@vue/cli-plugin-unit-mocha": "^4.5.8", 30 | "@vue/cli-service": "^4.5.8", 31 | "@vue/eslint-config-airbnb": "^5.1.0", 32 | "@vue/test-utils": "^1.1.1", 33 | "babel-eslint": "^10.1.0", 34 | "autoprefixer": "^6.7.2", 35 | "babel-core": "^6.26.3", 36 | "babel-loader": "^6.4.1", 37 | "babel-plugin-transform-runtime": "^6.22.0", 38 | "babel-preset-env": "^1.7.0", 39 | "babel-preset-stage-2": "^6.22.0", 40 | "babel-register": "^6.22.0", 41 | "chalk": "^1.1.3", 42 | "connect-history-api-fallback": "^1.3.0", 43 | "copy-webpack-plugin": "^4.0.1", 44 | "cross-env": "^5.0.0", 45 | "css-loader": "^0.28.0", 46 | "eslint": "^7.12.1", 47 | "eslint-config-airbnb": "^18.2.0", 48 | "eslint-loader": "4.0.2", 49 | "eslint-plugin-import": "^2.22.1", 50 | "eslint-plugin-prettier": "^3.1.4", 51 | "eslint-plugin-vue": "^7.1.0", 52 | "eventsource-polyfill": "^0.9.6", 53 | "express": "^4.14.1", 54 | "extract-text-webpack-plugin": "^2.0.0", 55 | "file-loader": "^0.11.1", 56 | "friendly-errors-webpack-plugin": "^1.1.3", 57 | "highlight.js": "^10.4.1", 58 | "html-webpack-plugin": "^2.28.0", 59 | "http-proxy-middleware": "^0.19.1", 60 | "less": "^3.9.0", 61 | "less-loader": "^4.1.0", 62 | "opn": "^4.0.2", 63 | "optimize-css-assets-webpack-plugin": "^1.3.0", 64 | "ora": "^1.2.0", 65 | "rimraf": "^2.6.0", 66 | "semver": "^5.3.0", 67 | "shelljs": "^0.8.3", 68 | "url-loader": "^1.1.2", 69 | "vue-eslint-parser": "^2.0.3", 70 | "vue-loader": "^11.3.4", 71 | "vue-router": "^2.5.3", 72 | "vue-style-loader": "^2.0.5", 73 | "vue-template-compiler": "^2.2.6", 74 | "webpack": "^2.3.3", 75 | "webpack-bundle-analyzer": ">=3.3.2", 76 | "webpack-dev-middleware": "^1.10.0", 77 | "webpack-hot-middleware": "^2.18.0", 78 | "webpack-merge": "^4.1.0" 79 | }, 80 | "repository": { 81 | "type": "git", 82 | "url": "https://github.com/mudin/vue-vr.git" 83 | }, 84 | "bugs": { 85 | "url": "https://github.com/mudin/vue-vr/issues" 86 | }, 87 | "license": "MIT", 88 | "engines": { 89 | "node": ">= 4.0.0", 90 | "npm": ">= 3.0.0" 91 | }, 92 | "browserslist": [ 93 | "> 1%", 94 | "last 2 versions", 95 | "not ie < 11" 96 | ] 97 | } 98 | -------------------------------------------------------------------------------- /src/Pano.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 129 | 130 | 139 | -------------------------------------------------------------------------------- /src/Scene.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 418 | -------------------------------------------------------------------------------- /src/Tour.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 418 | -------------------------------------------------------------------------------- /src/assets/hotspot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mudin/vue-vr/883761af5a24951bd6dc3928066d5813453106f9/src/assets/hotspot.png -------------------------------------------------------------------------------- /src/controls/OrbitControls.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | 3 | // https://github.com/mrdoob/three.js/blob/dev/demos/js/controls/OrbitControls.js 4 | 5 | const OrbitControls = function (object, domElement) { 6 | this.object = object 7 | 8 | this.domElement = (domElement !== undefined) ? domElement : document 9 | 10 | // Set to false to disable this control 11 | this.enabled = true 12 | 13 | // "target" sets the location of focus, where the object orbits around 14 | this.target = new THREE.Vector3() 15 | 16 | // How far you can dolly in and out ( PerspectiveCamera only ) 17 | this.minDistance = 0 18 | this.maxDistance = Infinity 19 | 20 | // How far you can zoom in and out ( OrthographicCamera only ) 21 | this.minZoom = 0 22 | this.maxZoom = Infinity 23 | 24 | // How far you can orbit vertically, upper and lower limits. 25 | // Range is 0 to Math.PI radians. 26 | this.minPolarAngle = 0 // radians 27 | this.maxPolarAngle = Math.PI // radians 28 | 29 | // How far you can orbit horizontally, upper and lower limits. 30 | // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. 31 | this.minAzimuthAngle = -Infinity // radians 32 | this.maxAzimuthAngle = Infinity // radians 33 | 34 | // Set to true to enable damping (inertia) 35 | // If damping is enabled, you must call controls.update() in your animation loop 36 | this.enableDamping = false 37 | this.dampingFactor = 0.25 38 | 39 | // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. 40 | // Set to false to disable zooming 41 | this.enableZoom = true 42 | this.zoomSpeed = 1.0 43 | 44 | // Set to false to disable rotating 45 | this.enableRotate = true 46 | this.rotateSpeed = 1.0 47 | 48 | // Set to false to disable panning 49 | this.enablePan = true 50 | this.keyPanSpeed = 7.0 // pixels moved per arrow key push 51 | 52 | // Set to true to automatically rotate around the target 53 | // If auto-rotate is enabled, you must call controls.update() in your animation loop 54 | this.autoRotate = false 55 | this.autoRotateSpeed = 2.0 // 30 seconds per round when fps is 60 56 | 57 | // Set to false to disable use of the keys 58 | this.enableKeys = true 59 | 60 | // The four arrow keys 61 | this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 } 62 | 63 | // Mouse buttons 64 | this.mouseButtons = { ORBIT: THREE.MOUSE.LEFT, ZOOM: THREE.MOUSE.MIDDLE, PAN: THREE.MOUSE.RIGHT } 65 | 66 | // for reset 67 | this.target0 = this.target.clone() 68 | this.position0 = this.object.position.clone() 69 | this.zoom0 = this.object.zoom 70 | 71 | // 72 | // public methods 73 | // 74 | 75 | this.getPolarAngle = function () { 76 | return spherical.phi 77 | } 78 | 79 | this.getAzimuthalAngle = function () { 80 | return spherical.theta 81 | } 82 | 83 | this.saveState = function () { 84 | scope.target0.copy(scope.target) 85 | scope.position0.copy(scope.object.position) 86 | scope.zoom0 = scope.object.zoom 87 | } 88 | 89 | this.reset = function () { 90 | scope.target.copy(scope.target0) 91 | scope.object.position.copy(scope.position0) 92 | scope.object.zoom = scope.zoom0 93 | 94 | scope.object.updateProjectionMatrix() 95 | scope.dispatchEvent(changeEvent) 96 | 97 | scope.update() 98 | 99 | state = STATE.NONE 100 | } 101 | 102 | // this method is exposed, but perhaps it would be better if we can make it private... 103 | this.update = (function () { 104 | var offset = new THREE.Vector3() 105 | 106 | // so camera.up is the orbit axis 107 | var quat = new THREE.Quaternion().setFromUnitVectors(object.up, new THREE.Vector3(0, 1, 0)) 108 | var quatInverse = quat.clone().inverse() 109 | 110 | var lastPosition = new THREE.Vector3() 111 | var lastQuaternion = new THREE.Quaternion() 112 | 113 | return function update () { 114 | var position = scope.object.position 115 | 116 | offset.copy(position).sub(scope.target) 117 | 118 | // rotate offset to "y-axis-is-up" space 119 | offset.applyQuaternion(quat) 120 | 121 | // angle from z-axis around y-axis 122 | spherical.setFromVector3(offset) 123 | 124 | if (scope.autoRotate && state === STATE.NONE) { 125 | rotateLeft(getAutoRotationAngle()) 126 | } 127 | 128 | spherical.theta += sphericalDelta.theta 129 | spherical.phi += sphericalDelta.phi 130 | 131 | // restrict theta to be between desired limits 132 | spherical.theta = Math.max(scope.minAzimuthAngle, Math.min(scope.maxAzimuthAngle, spherical.theta)) 133 | 134 | // restrict phi to be between desired limits 135 | spherical.phi = Math.max(scope.minPolarAngle, Math.min(scope.maxPolarAngle, spherical.phi)) 136 | 137 | spherical.makeSafe() 138 | 139 | spherical.radius *= scale 140 | 141 | // restrict radius to be between desired limits 142 | spherical.radius = Math.max(scope.minDistance, Math.min(scope.maxDistance, spherical.radius)) 143 | 144 | // move target to panned location 145 | scope.target.add(panOffset) 146 | 147 | offset.setFromSpherical(spherical) 148 | 149 | // rotate offset back to "camera-up-vector-is-up" space 150 | offset.applyQuaternion(quatInverse) 151 | 152 | position.copy(scope.target).add(offset) 153 | 154 | scope.object.lookAt(scope.target) 155 | 156 | if (scope.enableDamping === true) { 157 | sphericalDelta.theta *= (1 - scope.dampingFactor) 158 | sphericalDelta.phi *= (1 - scope.dampingFactor) 159 | } else { 160 | sphericalDelta.set(0, 0, 0) 161 | } 162 | 163 | scale = 1 164 | panOffset.set(0, 0, 0) 165 | 166 | // update condition is: 167 | // min(camera displacement, camera rotation in radians)^2 > EPS 168 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 169 | 170 | if (zoomChanged || 171 | lastPosition.distanceToSquared(scope.object.position) > EPS || 172 | 8 * (1 - lastQuaternion.dot(scope.object.quaternion)) > EPS) { 173 | scope.dispatchEvent(changeEvent) 174 | 175 | lastPosition.copy(scope.object.position) 176 | lastQuaternion.copy(scope.object.quaternion) 177 | zoomChanged = false 178 | 179 | return true 180 | } 181 | 182 | return false 183 | } 184 | }()) 185 | 186 | this.dispose = function () { 187 | scope.domElement.removeEventListener('contextmenu', onContextMenu, false) 188 | scope.domElement.removeEventListener('mousedown', onMouseDown, false) 189 | scope.domElement.removeEventListener('wheel', onMouseWheel, false) 190 | 191 | scope.domElement.removeEventListener('touchstart', onTouchStart, false) 192 | scope.domElement.removeEventListener('touchend', onTouchEnd, false) 193 | scope.domElement.removeEventListener('touchmove', onTouchMove, false) 194 | 195 | document.removeEventListener('mousemove', onMouseMove, false) 196 | document.removeEventListener('mouseup', onMouseUp, false) 197 | 198 | window.removeEventListener('keydown', onKeyDown, false) 199 | 200 | // scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? 201 | } 202 | 203 | // 204 | // internals 205 | // 206 | 207 | var scope = this 208 | 209 | var changeEvent = { type: 'change' } 210 | var startEvent = { type: 'start' } 211 | var endEvent = { type: 'end' } 212 | 213 | var STATE = { NONE: -1, ROTATE: 0, DOLLY: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_DOLLY: 4, TOUCH_PAN: 5 } 214 | 215 | var state = STATE.NONE 216 | 217 | var EPS = 0.000001 218 | 219 | // current position in spherical coordinates 220 | var spherical = new THREE.Spherical() 221 | var sphericalDelta = new THREE.Spherical() 222 | 223 | var scale = 1 224 | var panOffset = new THREE.Vector3() 225 | var zoomChanged = false 226 | 227 | var rotateStart = new THREE.Vector2() 228 | var rotateEnd = new THREE.Vector2() 229 | var rotateDelta = new THREE.Vector2() 230 | 231 | var panStart = new THREE.Vector2() 232 | var panEnd = new THREE.Vector2() 233 | var panDelta = new THREE.Vector2() 234 | 235 | var dollyStart = new THREE.Vector2() 236 | var dollyEnd = new THREE.Vector2() 237 | var dollyDelta = new THREE.Vector2() 238 | 239 | function getAutoRotationAngle () { 240 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed 241 | } 242 | 243 | function getZoomScale () { 244 | return Math.pow(0.95, scope.zoomSpeed) 245 | } 246 | 247 | function rotateLeft (angle) { 248 | sphericalDelta.theta -= angle 249 | } 250 | 251 | function rotateUp (angle) { 252 | sphericalDelta.phi -= angle 253 | } 254 | 255 | var panLeft = (function () { 256 | var v = new THREE.Vector3() 257 | 258 | return function panLeft (distance, objectMatrix) { 259 | v.setFromMatrixColumn(objectMatrix, 0) // get X column of objectMatrix 260 | v.multiplyScalar(-distance) 261 | 262 | panOffset.add(v) 263 | } 264 | }()) 265 | 266 | var panUp = (function () { 267 | var v = new THREE.Vector3() 268 | 269 | return function panUp (distance, objectMatrix) { 270 | v.setFromMatrixColumn(objectMatrix, 1) // get Y column of objectMatrix 271 | v.multiplyScalar(distance) 272 | 273 | panOffset.add(v) 274 | } 275 | }()) 276 | 277 | // deltaX and deltaY are in pixels; right and down are positive 278 | var pan = (function () { 279 | var offset = new THREE.Vector3() 280 | 281 | return function pan (deltaX, deltaY) { 282 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement 283 | 284 | if (scope.object instanceof THREE.PerspectiveCamera) { 285 | // perspective 286 | var position = scope.object.position 287 | offset.copy(position).sub(scope.target) 288 | var targetDistance = offset.length() 289 | 290 | // half of the fov is center to top of screen 291 | targetDistance *= Math.tan((scope.object.fov / 2) * Math.PI / 180.0) 292 | 293 | // we actually don't use screenWidth, since perspective camera is fixed to screen height 294 | panLeft(2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix) 295 | panUp(2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix) 296 | } else if (scope.object instanceof THREE.OrthographicCamera) { 297 | // orthographic 298 | panLeft(deltaX * (scope.object.right - scope.object.left) / scope.object.zoom / element.clientWidth, scope.object.matrix) 299 | panUp(deltaY * (scope.object.top - scope.object.bottom) / scope.object.zoom / element.clientHeight, scope.object.matrix) 300 | } else { 301 | // camera neither orthographic nor perspective 302 | console.warn('WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.') 303 | scope.enablePan = false 304 | } 305 | } 306 | }()) 307 | 308 | function dollyIn (dollyScale) { 309 | if (scope.object instanceof THREE.PerspectiveCamera) { 310 | scale /= dollyScale 311 | } else if (scope.object instanceof THREE.OrthographicCamera) { 312 | scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom * dollyScale)) 313 | scope.object.updateProjectionMatrix() 314 | zoomChanged = true 315 | } else { 316 | console.warn('WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.') 317 | scope.enableZoom = false 318 | } 319 | } 320 | 321 | function dollyOut (dollyScale) { 322 | if (scope.object instanceof THREE.PerspectiveCamera) { 323 | scale *= dollyScale 324 | } else if (scope.object instanceof THREE.OrthographicCamera) { 325 | scope.object.zoom = Math.max(scope.minZoom, Math.min(scope.maxZoom, scope.object.zoom / dollyScale)) 326 | scope.object.updateProjectionMatrix() 327 | zoomChanged = true 328 | } else { 329 | console.warn('WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.') 330 | scope.enableZoom = false 331 | } 332 | } 333 | 334 | // 335 | // event callbacks - update the object state 336 | // 337 | 338 | function handleMouseDownRotate (event) { 339 | // console.log( 'handleMouseDownRotate' ); 340 | 341 | rotateStart.set(event.clientX, event.clientY) 342 | } 343 | 344 | function handleMouseDownDolly (event) { 345 | // console.log( 'handleMouseDownDolly' ); 346 | 347 | dollyStart.set(event.clientX, event.clientY) 348 | } 349 | 350 | function handleMouseDownPan (event) { 351 | // console.log( 'handleMouseDownPan' ); 352 | 353 | panStart.set(event.clientX, event.clientY) 354 | } 355 | 356 | function handleMouseMoveRotate (event) { 357 | // console.log( 'handleMouseMoveRotate' ); 358 | 359 | rotateEnd.set(event.clientX, event.clientY) 360 | rotateDelta.subVectors(rotateEnd, rotateStart) 361 | 362 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement 363 | 364 | // rotating across whole screen goes 360 degrees around 365 | rotateLeft(2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed) 366 | 367 | // rotating up and down along whole screen attempts to go 360, but limited to 180 368 | rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed) 369 | 370 | rotateStart.copy(rotateEnd) 371 | 372 | scope.update() 373 | } 374 | 375 | function handleMouseMoveDolly (event) { 376 | // console.log( 'handleMouseMoveDolly' ); 377 | 378 | dollyEnd.set(event.clientX, event.clientY) 379 | 380 | dollyDelta.subVectors(dollyEnd, dollyStart) 381 | 382 | if (dollyDelta.y > 0) { 383 | dollyIn(getZoomScale()) 384 | } else if (dollyDelta.y < 0) { 385 | dollyOut(getZoomScale()) 386 | } 387 | 388 | dollyStart.copy(dollyEnd) 389 | 390 | scope.update() 391 | } 392 | 393 | function handleMouseMovePan (event) { 394 | // console.log( 'handleMouseMovePan' ); 395 | 396 | panEnd.set(event.clientX, event.clientY) 397 | 398 | panDelta.subVectors(panEnd, panStart) 399 | 400 | pan(panDelta.x, panDelta.y) 401 | 402 | panStart.copy(panEnd) 403 | 404 | scope.update() 405 | } 406 | 407 | function handleMouseUp (event) { 408 | 409 | // console.log( 'handleMouseUp' ); 410 | 411 | } 412 | 413 | function handleMouseWheel (event) { 414 | // console.log( 'handleMouseWheel' ); 415 | 416 | if (event.deltaY < 0) { 417 | dollyOut(getZoomScale()) 418 | } else if (event.deltaY > 0) { 419 | dollyIn(getZoomScale()) 420 | } 421 | 422 | scope.update() 423 | } 424 | 425 | function handleKeyDown (event) { 426 | // console.log( 'handleKeyDown' ); 427 | 428 | switch (event.keyCode) { 429 | case scope.keys.UP: 430 | pan(0, scope.keyPanSpeed) 431 | scope.update() 432 | break 433 | 434 | case scope.keys.BOTTOM: 435 | pan(0, -scope.keyPanSpeed) 436 | scope.update() 437 | break 438 | 439 | case scope.keys.LEFT: 440 | pan(scope.keyPanSpeed, 0) 441 | scope.update() 442 | break 443 | 444 | case scope.keys.RIGHT: 445 | pan(-scope.keyPanSpeed, 0) 446 | scope.update() 447 | break 448 | } 449 | } 450 | 451 | function handleTouchStartRotate (event) { 452 | // console.log( 'handleTouchStartRotate' ); 453 | 454 | rotateStart.set(event.touches[ 0 ].pageX, event.touches[ 0 ].pageY) 455 | } 456 | 457 | function handleTouchStartDolly (event) { 458 | // console.log( 'handleTouchStartDolly' ); 459 | 460 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX 461 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY 462 | 463 | var distance = Math.sqrt(dx * dx + dy * dy) 464 | 465 | dollyStart.set(0, distance) 466 | } 467 | 468 | function handleTouchStartPan (event) { 469 | // console.log( 'handleTouchStartPan' ); 470 | 471 | panStart.set(event.touches[ 0 ].pageX, event.touches[ 0 ].pageY) 472 | } 473 | 474 | function handleTouchMoveRotate (event) { 475 | // console.log( 'handleTouchMoveRotate' ); 476 | 477 | rotateEnd.set(event.touches[ 0 ].pageX, event.touches[ 0 ].pageY) 478 | rotateDelta.subVectors(rotateEnd, rotateStart) 479 | 480 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement 481 | 482 | // rotating across whole screen goes 360 degrees around 483 | rotateLeft(2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed) 484 | 485 | // rotating up and down along whole screen attempts to go 360, but limited to 180 486 | rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed) 487 | 488 | rotateStart.copy(rotateEnd) 489 | 490 | scope.update() 491 | } 492 | 493 | function handleTouchMoveDolly (event) { 494 | // console.log( 'handleTouchMoveDolly' ); 495 | 496 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX 497 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY 498 | 499 | var distance = Math.sqrt(dx * dx + dy * dy) 500 | 501 | dollyEnd.set(0, distance) 502 | 503 | dollyDelta.subVectors(dollyEnd, dollyStart) 504 | 505 | if (dollyDelta.y > 0) { 506 | dollyOut(getZoomScale()) 507 | } else if (dollyDelta.y < 0) { 508 | dollyIn(getZoomScale()) 509 | } 510 | 511 | dollyStart.copy(dollyEnd) 512 | 513 | scope.update() 514 | } 515 | 516 | function handleTouchMovePan (event) { 517 | // console.log( 'handleTouchMovePan' ); 518 | 519 | panEnd.set(event.touches[ 0 ].pageX, event.touches[ 0 ].pageY) 520 | 521 | panDelta.subVectors(panEnd, panStart) 522 | 523 | pan(panDelta.x, panDelta.y) 524 | 525 | panStart.copy(panEnd) 526 | 527 | scope.update() 528 | } 529 | 530 | function handleTouchEnd (event) { 531 | 532 | // console.log( 'handleTouchEnd' ); 533 | 534 | } 535 | 536 | // 537 | // event handlers - FSM: listen for events and reset state 538 | // 539 | 540 | function onMouseDown (event) { 541 | if (scope.enabled === false) return 542 | 543 | event.preventDefault() 544 | 545 | switch (event.button) { 546 | case scope.mouseButtons.ORBIT: 547 | 548 | if (scope.enableRotate === false) return 549 | 550 | handleMouseDownRotate(event) 551 | 552 | state = STATE.ROTATE 553 | 554 | break 555 | 556 | case scope.mouseButtons.ZOOM: 557 | 558 | if (scope.enableZoom === false) return 559 | 560 | handleMouseDownDolly(event) 561 | 562 | state = STATE.DOLLY 563 | 564 | break 565 | 566 | case scope.mouseButtons.PAN: 567 | 568 | if (scope.enablePan === false) return 569 | 570 | handleMouseDownPan(event) 571 | 572 | state = STATE.PAN 573 | 574 | break 575 | } 576 | 577 | if (state !== STATE.NONE) { 578 | document.addEventListener('mousemove', onMouseMove, false) 579 | document.addEventListener('mouseup', onMouseUp, false) 580 | 581 | scope.dispatchEvent(startEvent) 582 | } 583 | } 584 | 585 | function onMouseMove (event) { 586 | if (scope.enabled === false) return 587 | 588 | event.preventDefault() 589 | 590 | switch (state) { 591 | case STATE.ROTATE: 592 | 593 | if (scope.enableRotate === false) return 594 | 595 | handleMouseMoveRotate(event) 596 | 597 | break 598 | 599 | case STATE.DOLLY: 600 | 601 | if (scope.enableZoom === false) return 602 | 603 | handleMouseMoveDolly(event) 604 | 605 | break 606 | 607 | case STATE.PAN: 608 | 609 | if (scope.enablePan === false) return 610 | 611 | handleMouseMovePan(event) 612 | 613 | break 614 | } 615 | } 616 | 617 | function onMouseUp (event) { 618 | if (scope.enabled === false) return 619 | 620 | handleMouseUp(event) 621 | 622 | document.removeEventListener('mousemove', onMouseMove, false) 623 | document.removeEventListener('mouseup', onMouseUp, false) 624 | 625 | scope.dispatchEvent(endEvent) 626 | 627 | state = STATE.NONE 628 | } 629 | 630 | function onMouseWheel (event) { 631 | if (scope.enabled === false || scope.enableZoom === false || (state !== STATE.NONE && state !== STATE.ROTATE)) return 632 | 633 | event.preventDefault() 634 | event.stopPropagation() 635 | 636 | handleMouseWheel(event) 637 | 638 | scope.dispatchEvent(startEvent) // not sure why these are here... 639 | scope.dispatchEvent(endEvent) 640 | } 641 | 642 | function onKeyDown (event) { 643 | if (scope.enabled === false || scope.enableKeys === false || scope.enablePan === false) return 644 | 645 | handleKeyDown(event) 646 | } 647 | 648 | function onTouchStart (event) { 649 | if (scope.enabled === false) return 650 | 651 | switch (event.touches.length) { 652 | case 1: // one-fingered touch: rotate 653 | 654 | if (scope.enableRotate === false) return 655 | 656 | handleTouchStartRotate(event) 657 | 658 | state = STATE.TOUCH_ROTATE 659 | 660 | break 661 | 662 | case 2: // two-fingered touch: dolly 663 | 664 | if (scope.enableZoom === false) return 665 | 666 | handleTouchStartDolly(event) 667 | 668 | state = STATE.TOUCH_DOLLY 669 | 670 | break 671 | 672 | case 3: // three-fingered touch: pan 673 | 674 | if (scope.enablePan === false) return 675 | 676 | handleTouchStartPan(event) 677 | 678 | state = STATE.TOUCH_PAN 679 | 680 | break 681 | 682 | default: 683 | 684 | state = STATE.NONE 685 | } 686 | 687 | if (state !== STATE.NONE) { 688 | scope.dispatchEvent(startEvent) 689 | } 690 | } 691 | 692 | function onTouchMove (event) { 693 | if (scope.enabled === false) return 694 | 695 | event.preventDefault() 696 | event.stopPropagation() 697 | 698 | switch (event.touches.length) { 699 | case 1: // one-fingered touch: rotate 700 | 701 | if (scope.enableRotate === false) return 702 | if (state !== STATE.TOUCH_ROTATE) return // is this needed?... 703 | 704 | handleTouchMoveRotate(event) 705 | 706 | break 707 | 708 | case 2: // two-fingered touch: dolly 709 | 710 | if (scope.enableZoom === false) return 711 | if (state !== STATE.TOUCH_DOLLY) return // is this needed?... 712 | 713 | handleTouchMoveDolly(event) 714 | 715 | break 716 | 717 | case 3: // three-fingered touch: pan 718 | 719 | if (scope.enablePan === false) return 720 | if (state !== STATE.TOUCH_PAN) return // is this needed?... 721 | 722 | handleTouchMovePan(event) 723 | 724 | break 725 | 726 | default: 727 | 728 | state = STATE.NONE 729 | } 730 | } 731 | 732 | function onTouchEnd (event) { 733 | if (scope.enabled === false) return 734 | 735 | handleTouchEnd(event) 736 | 737 | scope.dispatchEvent(endEvent) 738 | 739 | state = STATE.NONE 740 | } 741 | 742 | function onContextMenu (event) { 743 | event.preventDefault() 744 | } 745 | 746 | // 747 | 748 | scope.domElement.addEventListener('contextmenu', onContextMenu, false) 749 | 750 | scope.domElement.addEventListener('mousedown', onMouseDown, false) 751 | scope.domElement.addEventListener('wheel', onMouseWheel, false) 752 | 753 | scope.domElement.addEventListener('touchstart', onTouchStart, false) 754 | scope.domElement.addEventListener('touchend', onTouchEnd, false) 755 | scope.domElement.addEventListener('touchmove', onTouchMove, false) 756 | 757 | window.addEventListener('keydown', onKeyDown, false) 758 | 759 | // force an update at start 760 | 761 | this.update() 762 | } 763 | 764 | OrbitControls.prototype = Object.create(THREE.EventDispatcher.prototype) 765 | OrbitControls.prototype.constructor = OrbitControls 766 | 767 | export { OrbitControls } 768 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Pano from './Pano.vue'; 3 | import Scene from './Scene.vue'; 4 | import Tour from './Tour.vue'; 5 | 6 | const components = [Pano, Tour, Scene]; 7 | 8 | const install = Vue => { 9 | components.map(component => { 10 | Vue.component(component.name, component); 11 | }); 12 | }; 13 | 14 | if (typeof window !== 'undefined' && window.Vue) { 15 | install(window.Vue); 16 | } 17 | 18 | export { install, Pano, Scene, Tour }; 19 | -------------------------------------------------------------------------------- /src/lib/hotspots.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @author Pablo Acuña / pbk.pablo.a@gmail.com/, 3 | * @author Jorge Mayoraz / jorge.emh@hotmail.com/, 4 | * @author Luciano Rodriguez / luciano.rdz@gmail.com / 5 | */ 6 | import * as THREE from 'three' 7 | 8 | function isFunction(functionToCheck) { 9 | var getType = {}; 10 | return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]'; 11 | } 12 | 13 | function generateUUID() { 14 | var d = new Date().getTime(); 15 | var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 16 | var r = (d + Math.random() * 16) % 16 | 0; 17 | d = Math.floor(d / 16); 18 | return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16); 19 | }); 20 | return uuid; 21 | }; 22 | 23 | 24 | THREE.HotspotGlobals = { 25 | hotspotList: [], 26 | init: function() { 27 | var HTSPTG = this; 28 | var raycaster = new THREE.Raycaster(); 29 | var mouse = new THREE.Vector3(); 30 | var obj = {}; 31 | var canDoClick = false; 32 | var contPos = getPos(HTSPTG.container); 33 | var canvas = this.container; 34 | 35 | this.over = false; 36 | this.down = false; 37 | 38 | canvas.addEventListener('mousedown', function(e) { 39 | var _event = {}; 40 | _event.clientX = e.clientX - contPos.x; 41 | _event.clientY = e.clientY - contPos.y; 42 | onMouseDown(_event); 43 | }, false); 44 | 45 | canvas.addEventListener("mouseup", function(e) { 46 | var _event = {}; 47 | _event.clientX = e.clientX - contPos.x; 48 | _event.clientY = e.clientY - contPos.y; 49 | onMouseUp(_event); 50 | }); 51 | 52 | canvas.addEventListener("mousemove", function(e) { 53 | var _event = {}; 54 | _event.clientX = e.clientX - contPos.x; 55 | _event.clientY = e.clientY - contPos.y; 56 | onMouseMove(_event); 57 | }); 58 | 59 | canvas.addEventListener('touchstart', function(e) { 60 | var _event = {}; 61 | _event.clientX = e.changedTouches[0].pageX - contPos.x; 62 | _event.clientY = e.changedTouches[0].pageY - contPos.y; 63 | onMouseMove(_event); 64 | onMouseDown(_event); 65 | }, false); 66 | 67 | canvas.addEventListener('touchend', function(e) { 68 | var _event = {}; 69 | _event.clientX = e.changedTouches[0].pageX - contPos.x; 70 | _event.clientY = e.changedTouches[0].pageY - contPos.y; 71 | onMouseUp(_event); 72 | }, false); 73 | 74 | 75 | function onMouseDown(event) { 76 | mouse.x = (event.clientX - (HTSPTG.container.offsetWidth / 2)); 77 | mouse.y = -(event.clientY - (HTSPTG.container.offsetHeight / 2)); 78 | mouse.z = 10; 79 | 80 | HTSPTG.down = false; 81 | 82 | var dest = new THREE.Vector3(0, 0, -1); 83 | raycaster.set(mouse, dest); 84 | var intersects = raycaster.intersectObjects(HTSPTG.hotspotList); 85 | if (intersects.length > 0) { 86 | for (var i = 0; i < intersects.length; i++) { 87 | var intersection = intersects[i]; 88 | obj = intersection.object; 89 | 90 | if (obj.controller.itsActive > 0 && obj.material.opacity) { 91 | obj.material.color.setHex(0xb2b2b2); 92 | 93 | if (isFunction(HTSPTG.over.controller.onMouseDown)) { 94 | HTSPTG.over.controller.onMouseDown(); 95 | } else if (isFunction(obj.controller.mouseDown)) { 96 | obj.controller.mouseDown(); 97 | } 98 | 99 | if (obj.controller.isBind()) { 100 | HTSPTG.down = obj; 101 | } 102 | 103 | break; 104 | } 105 | } 106 | } 107 | 108 | }; 109 | 110 | function onMouseMove(event) { 111 | 112 | mouse.x = (event.clientX - (HTSPTG.container.offsetWidth / 2)); 113 | mouse.y = -(event.clientY - (HTSPTG.container.offsetHeight / 2)); 114 | mouse.z = 10; 115 | 116 | var dest = new THREE.Vector3(0, 0, -1); 117 | raycaster.set(mouse, dest); 118 | var intersects = raycaster.intersectObjects(HTSPTG.hotspotList); 119 | if (intersects.length > 0) { 120 | 121 | var oks = false; 122 | 123 | for (var i = 0; i < intersects.length; i++) { 124 | var intersection = intersects[i]; 125 | obj = intersection.object; 126 | 127 | if (obj.controller.itsActive > 0 && obj.material.opacity) { 128 | if (HTSPTG.over) { 129 | if (HTSPTG.over.controller.selector != obj.controller.selector) { 130 | if (isFunction(HTSPTG.over.controller.onMouseOut)) { 131 | HTSPTG.over.controller.onMouseOut(); 132 | } 133 | HTSPTG.over = false; 134 | } 135 | 136 | } 137 | 138 | if (isFunction(obj.controller.onMouseOver)) { 139 | obj.controller.onMouseOver(); 140 | } else if (isFunction(obj.controller.mouseOver)) { 141 | obj.controller.mouseOver(); 142 | } 143 | 144 | HTSPTG.over = obj; 145 | oks = true; 146 | 147 | break; 148 | } 149 | 150 | } 151 | 152 | if (!oks) { 153 | if (HTSPTG.over) { 154 | if (isFunction(HTSPTG.over.controller.onMouseOut)) { 155 | HTSPTG.over.controller.onMouseOut(); 156 | } else if (isFunction(obj.controller.mouseOut)) { 157 | obj.controller.mouseOut(); 158 | } 159 | HTSPTG.over = false; 160 | } 161 | } 162 | 163 | } else if (HTSPTG.over) { 164 | if (isFunction(HTSPTG.over.controller.onMouseOut)) { 165 | HTSPTG.over.controller.onMouseOut(); 166 | } 167 | HTSPTG.over = false; 168 | } 169 | }; 170 | 171 | 172 | function onMouseUp(event) { 173 | 174 | mouse.x = (event.clientX - (HTSPTG.container.offsetWidth / 2)); 175 | mouse.y = -(event.clientY - (HTSPTG.container.offsetHeight / 2)); 176 | mouse.z = 10; 177 | 178 | var dest = new THREE.Vector3(0, 0, -1); 179 | raycaster.set(mouse, dest); 180 | var intersects = raycaster.intersectObjects(HTSPTG.hotspotList); 181 | if (intersects.length > 0) { 182 | for (var i = 0; i < intersects.length; i++) { 183 | var intersection = intersects[i]; 184 | obj = intersection.object; 185 | 186 | if (obj.controller.itsActive > 0 && obj.material.opacity) { 187 | if (isFunction(obj.controller.onMouseUp)) { 188 | obj.controller.onMouseUp(); 189 | } else if (isFunction(obj.controller.mouseUp)) { 190 | obj.controller.mouseUp(); 191 | } 192 | 193 | if (!HTSPTG.down) { 194 | return; 195 | } 196 | 197 | if (!obj.controller.isBind()) { 198 | HTSPTG.down = false; 199 | return; 200 | } 201 | 202 | if (obj.material) { 203 | obj.material.color.setHex(0xffffff); 204 | } 205 | 206 | if (HTSPTG.down.controller.selector == obj.controller.selector) { 207 | HTSPTG.down = false; 208 | 209 | if (isFunction(obj.controller.onClick)) { 210 | obj.controller.onClick(); 211 | } else if (isFunction(obj.controller.click)) { 212 | obj.controller.click(); 213 | } 214 | } 215 | 216 | break; 217 | } 218 | } 219 | } 220 | 221 | 222 | 223 | }; 224 | 225 | function getPos(el) { 226 | // yay readability 227 | for (var lx = 0, ly = 0; el != null; lx += el.offsetLeft, ly += el.offsetTop, el = el.offsetParent); 228 | return { 229 | x: lx, 230 | y: ly 231 | }; 232 | }; 233 | HTSPTG.initialized = true; 234 | }, 235 | onResizeFunc: function() { 236 | this.orthoCamera.left = -this.container.offsetWidth / 2; 237 | this.orthoCamera.right = this.container.offsetWidth / 2; 238 | this.orthoCamera.top = this.container.offsetHeight / 2; 239 | this.orthoCamera.bottom = -this.container.offsetHeight / 2; 240 | this.orthoCamera.updateProjectionMatrix(); 241 | }, 242 | update: function() { 243 | this.renderer.clearDepth(); 244 | this.renderer.render(this.orthoScene, this.orthoCamera); 245 | } 246 | }; 247 | 248 | THREE.threeDataConfig = {}; 249 | 250 | THREE.Hotspot = function(imgURL, rangeAngle, directionAngle) { 251 | THREE.Object3D.call(this); 252 | 253 | // API 254 | this.itsActive = true; 255 | this.offset = { 256 | x: 0, 257 | y: 0 258 | }; 259 | this.pivotPoint = THREE.HotspotPivotPoints.BOTTOM_LEFT; 260 | 261 | // Internals 262 | this.hsptImg; 263 | this.selector = generateUUID(); // asign uuid 264 | this.rangeAngle = rangeAngle; 265 | this.directionAngle = directionAngle; 266 | this.alpha = 1; 267 | 268 | // by global 269 | this.renderer = THREE.threeDataConfig.renderer; 270 | this.camera = THREE.threeDataConfig.camera; 271 | this.container = THREE.threeDataConfig.renderer.domElement; 272 | 273 | if (!THREE.HotspotGlobals.orthoCamera) { 274 | //console.log('pixel ratio: ', window.devicePixelRatio); 275 | if (window.devicePixelRatio > 1) { 276 | THREE.HotspotGlobals.orthoCamera = new THREE.OrthographicCamera(-this.container.offsetWidth / 2, this.container.offsetWidth / 2, this.container.offsetHeight / 2 + 50, -this.container.offsetHeight / 2 - 50, 1, 1000000); 277 | } else { 278 | THREE.HotspotGlobals.orthoCamera = new THREE.OrthographicCamera(-this.container.offsetWidth / 2, this.container.offsetWidth / 2, this.container.offsetHeight / 2, -this.container.offsetHeight / 2, 1, 1000000); 279 | } 280 | THREE.HotspotGlobals.orthoCamera.position.z = 10; 281 | } 282 | if (!THREE.HotspotGlobals.orthoScene) { 283 | THREE.HotspotGlobals.orthoScene = new THREE.Scene(); 284 | } 285 | if (!THREE.HotspotGlobals.container) { 286 | THREE.HotspotGlobals.container = THREE.threeDataConfig.renderer.domElement; 287 | } 288 | if (!THREE.HotspotGlobals.renderer) { 289 | THREE.HotspotGlobals.renderer = THREE.threeDataConfig.renderer; 290 | } 291 | if (!THREE.HotspotGlobals.initialized) { 292 | THREE.HotspotGlobals.init(this.container); 293 | } 294 | 295 | var width, height; 296 | var HP = this; 297 | var material = new THREE.SpriteMaterial({ 298 | map: THREE.ImageUtils.loadTexture(imgURL, undefined, function() { 299 | HP.hosptWidth = material.map.image.width / 2; 300 | HP.hosptHeight = material.map.image.height / 2; 301 | HP.hsptImg.scale.set(HP.hosptWidth, HP.hosptHeight, 1); 302 | }) 303 | }); 304 | this.hsptImg = new THREE.Sprite(material); 305 | this.hsptImg.controller = this; 306 | THREE.HotspotGlobals.orthoScene.add(this.hsptImg); 307 | THREE.HotspotGlobals.hotspotList.push(this.hsptImg); 308 | }; 309 | 310 | THREE.Hotspot.prototype = Object.create(THREE.Object3D.prototype); 311 | THREE.Hotspot.prototype.constructor = THREE.Hotspot; 312 | 313 | THREE.Hotspot.prototype.update = function() { 314 | this.updateElementPos(); 315 | this.updateElementAlpha(); 316 | this.checkIfBehindCamera(); 317 | }; 318 | 319 | 320 | THREE.Hotspot.prototype.isBind = function() { 321 | if (this.onClick) { 322 | return true 323 | } else if (this.click) { 324 | return true 325 | } else { 326 | return false; 327 | } 328 | }; 329 | 330 | 331 | THREE.Hotspot.prototype.updateElementPos = function() { 332 | 333 | var halfWidth = this.container.offsetWidth / 2; 334 | var halfHeight = this.container.offsetHeight / 2; 335 | 336 | if (this.autoUpdate != false) { 337 | var proj = this.toScreenPosition(); 338 | } 339 | 340 | var x = 0, 341 | y = 0; 342 | switch (this.pivotPoint) { 343 | case THREE.HotspotPivotPoints.TOP_LEFT: 344 | x = proj.x + (this.hosptWidth / 2); 345 | y = proj.y + (this.hosptHeight / 2); 346 | break; 347 | case THREE.HotspotPivotPoints.TOP: 348 | x = proj.x; 349 | y = proj.y + (this.hosptHeight / 2); 350 | break; 351 | case THREE.HotspotPivotPoints.TOP_RIGHT: 352 | x = proj.x - (this.hosptWidth / 2); 353 | y = proj.y + (this.hosptHeight / 2); 354 | break; 355 | case THREE.HotspotPivotPoints.RIGHT: 356 | x = proj.x - (this.hosptWidth / 2); 357 | y = proj.y; 358 | break; 359 | case THREE.HotspotPivotPoints.BOTTOM_RIGHT: 360 | x = proj.x - (this.hosptWidth / 2); 361 | y = proj.y - (this.hosptHeight / 2); 362 | break; 363 | case THREE.HotspotPivotPoints.BOTTOM: 364 | x = proj.x; 365 | y = proj.y - (this.hosptHeight / 2); 366 | break; 367 | case THREE.HotspotPivotPoints.BOTTOM_LEFT: 368 | x = proj.x + (this.hosptWidth / 2); 369 | y = proj.y - (this.hosptHeight / 2); 370 | break; 371 | case THREE.HotspotPivotPoints.LEFT: 372 | x = proj.x + (this.hosptWidth / 2); 373 | y = proj.y; 374 | break; 375 | case THREE.HotspotPivotPoints.CENTER: 376 | x = proj.x; 377 | y = proj.y; 378 | break; 379 | } 380 | 381 | var rpx = -(halfWidth - (x + this.offset.x)); 382 | var rpy = (halfHeight - (y - this.offset.y)); 383 | this.hsptImg.position.set(rpx, rpy, 0); 384 | }; 385 | 386 | THREE.Hotspot.prototype.toScreenPosition = function() { 387 | var vector = new THREE.Vector3(); 388 | 389 | this.widthHalf = 0.5 * this.renderer.context.canvas.width; 390 | this.heightHalf = 0.5 * this.renderer.context.canvas.height; 391 | 392 | this.updateMatrixWorld(); 393 | vector.setFromMatrixPosition(this.matrixWorld); 394 | vector.project(this.camera); 395 | 396 | vector.x = (vector.x * this.widthHalf) + this.widthHalf; 397 | vector.y = -(vector.y * this.heightHalf) + this.heightHalf; 398 | 399 | vector.x = (vector.x / window.devicePixelRatio); 400 | vector.y = (vector.y / window.devicePixelRatio); 401 | 402 | return { 403 | x: vector.x, 404 | y: vector.y 405 | }; 406 | }; 407 | 408 | 409 | THREE.Hotspot.prototype.updateElementAlpha = function() { 410 | if (this.itsActive) { 411 | if (this.rangeAngle !== 360) { 412 | var angleDeg = Math.atan2(this.camera.position.z - this.position.z, this.camera.position.x - this.position.x) * 180 / Math.PI; 413 | var angle = angleDeg + 90 + this.directionAngle; 414 | 415 | if (angle > 360) 416 | angle = -(angle - 360); 417 | if (angle < 0) 418 | angle = 360 + angle; 419 | 420 | if (angle >= 180) { 421 | if (angle <= (this.rangeAngle + 180)) 422 | this.alpha = (((Math.abs((this.rangeAngle + 180) - angle) * 100)) / this.rangeAngle) / 100; 423 | else 424 | this.alpha = 0; 425 | } else { 426 | if (angle >= (180 - this.rangeAngle)) 427 | this.alpha = (((Math.abs((360 - this.rangeAngle - 180) - angle) * 100)) / this.rangeAngle) / 100; 428 | else 429 | this.alpha = 0; 430 | } 431 | 432 | if (angle <= 0) 433 | this.alpha = 0; 434 | 435 | this.hsptImg.material.opacity = this.alpha; 436 | } else { 437 | this.hsptImg.material.opacity = 1; 438 | } 439 | } else { 440 | this.hsptImg.material.opacity = 0; 441 | } 442 | }; 443 | 444 | THREE.Hotspot.prototype.checkIfBehindCamera = function(){ 445 | var cameraFoward = this.camera.getWorldDirection(); 446 | var vectorToHotspot = new THREE.Vector3(); 447 | vectorToHotspot.subVectors(this.position, this.camera.position); 448 | if(cameraFoward.dot(vectorToHotspot) < 0){ 449 | this.hsptImg.visible = false; 450 | } else { 451 | this.hsptImg.visible = true; 452 | } 453 | } 454 | 455 | THREE.Hotspot.prototype.disable = function() { 456 | this.itsActive = false; 457 | }; 458 | 459 | THREE.Hotspot.prototype.enable = function() { 460 | this.itsActive = true; 461 | }; 462 | 463 | THREE.Hotspot.prototype.hide = function() { 464 | this.hsptImg.material.opacity = 0; 465 | this.originalItsActive = this.itsActive; 466 | this.itsActive = false; 467 | }; 468 | 469 | THREE.Hotspot.prototype.show = function() { 470 | this.hsptImg.material.opacity = 1; 471 | this.itsActive = this.originalItsActive || true; 472 | }; 473 | 474 | // retro-compatibility 475 | THREE.Hotspot.prototype.turnOff = function() { 476 | this.hsptImg.material.opacity = 0; 477 | this.originalItsActive = this.itsActive; 478 | this.itsActive = false; 479 | }; 480 | 481 | THREE.Hotspot.prototype.turnOn = function() { 482 | this.hsptImg.material.opacity = 1; 483 | this.itsActive = this.originalItsActive || true; 484 | }; 485 | 486 | 487 | THREE.HotspotPivotPoints = { 488 | TOP_LEFT: 0, 489 | TOP: 1, 490 | TOP_RIGHT: 2, 491 | RIGHT: 3, 492 | BOTTOM_RIGHT: 4, 493 | BOTTOM: 5, 494 | BOTTOM_LEFT: 6, 495 | LEFT: 7, 496 | CENTER: 8 497 | }; 498 | 499 | export default THREE 500 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | import { Box3, Vector3, Vector2, BufferAttribute } from 'three' 2 | 3 | let box = new Box3() 4 | 5 | function getSize (object) { 6 | box.setFromObject(object) 7 | 8 | return box.getSize() 9 | } 10 | 11 | function getCenter (object) { 12 | box.setFromObject(object) 13 | 14 | return box.getCenter() 15 | } 16 | 17 | function lightsDiff (lights, oldLights) { 18 | } 19 | 20 | function toIndexed (bufferGeometry) { 21 | let rawPositions = bufferGeometry.getAttribute('position').array 22 | 23 | let rawUvs 24 | let hasUV = bufferGeometry.getAttribute('uv') !== undefined 25 | if (hasUV) rawUvs = bufferGeometry.getAttribute('uv').array 26 | 27 | let rawNormals 28 | let hasNormal = bufferGeometry.getAttribute('normal') !== undefined 29 | if (hasNormal) rawNormals = bufferGeometry.getAttribute('normal').array 30 | 31 | let indices = [] 32 | let vertices = [] 33 | let normals = [] 34 | let uvs = [] 35 | 36 | let face, faceNormals, faceUvs, tmpIndices 37 | 38 | let v0 = new Vector3() 39 | let v1 = new Vector3() 40 | let v2 = new Vector3() 41 | 42 | let n0 = new Vector3() 43 | let n1 = new Vector3() 44 | let n2 = new Vector3() 45 | 46 | let uv0 = new Vector2() 47 | let uv1 = new Vector2() 48 | let uv2 = new Vector2() 49 | 50 | for (let i = 0; i < rawPositions.length; i += 9) { 51 | v0.x = rawPositions[ i ] 52 | v0.y = rawPositions[ i + 1 ] 53 | v0.z = rawPositions[ i + 2 ] 54 | 55 | v1.x = rawPositions[ i + 3 ] 56 | v1.y = rawPositions[ i + 4 ] 57 | v1.z = rawPositions[ i + 5 ] 58 | 59 | v2.x = rawPositions[ i + 6 ] 60 | v2.y = rawPositions[ i + 7 ] 61 | v2.z = rawPositions[ i + 8 ] 62 | 63 | face = [ v0, v1, v2 ] 64 | 65 | if (hasNormal) { 66 | n0.x = rawNormals[ i ] 67 | n0.y = rawNormals[ i + 1 ] 68 | n0.z = rawNormals[ i + 2 ] 69 | 70 | n1.x = rawNormals[ i + 3 ] 71 | n1.y = rawNormals[ i + 4 ] 72 | n1.z = rawNormals[ i + 5 ] 73 | 74 | n2.x = rawNormals[ i + 6 ] 75 | n2.y = rawNormals[ i + 7 ] 76 | n2.z = rawNormals[ i + 8 ] 77 | 78 | faceNormals = [ n0, n1, n2 ] 79 | } 80 | 81 | if (hasUV) { 82 | uv0.x = rawUvs[ i ] 83 | uv0.y = rawUvs[ i + 1 ] 84 | 85 | uv1.x = rawUvs[ i + 2 ] 86 | uv1.y = rawUvs[ i + 3 ] 87 | 88 | uv2.x = rawUvs[ i + 4 ] 89 | uv2.y = rawUvs[ i + 5 ] 90 | 91 | faceUvs = [ uv0, uv1, uv2 ] 92 | } 93 | 94 | tmpIndices = [] 95 | 96 | face.forEach(function (v, i) { 97 | let id = exists(v, vertices) 98 | if (id === -1) { 99 | id = vertices.length 100 | vertices.push(v.clone()) 101 | 102 | if (hasNormal) normals.push(faceNormals[ i ].clone()) 103 | if (hasUV) uvs.push(faceUvs[ i ].clone()) 104 | } 105 | tmpIndices.push(id) 106 | }) 107 | 108 | indices.push(tmpIndices[ 0 ], tmpIndices[ 1 ], tmpIndices[ 2 ]) 109 | } 110 | 111 | let normalBuffer 112 | let uvBuffer 113 | let positionBuffer = new Float32Array(vertices.length * 3) 114 | 115 | if (hasNormal) normalBuffer = new Float32Array(vertices.length * 3) 116 | if (hasUV) uvBuffer = new Float32Array(vertices.length * 2) 117 | 118 | let i2 = 0 119 | let i3 = 0 120 | for (i = 0; i < vertices.length; i++) { 121 | i3 = i * 3 122 | 123 | positionBuffer[ i3 ] = vertices[ i ].x 124 | positionBuffer[ i3 + 1 ] = vertices[ i ].y 125 | positionBuffer[ i3 + 2 ] = vertices[ i ].z 126 | 127 | if (hasNormal) { 128 | normalBuffer[ i3 ] = normals[ i ].x 129 | normalBuffer[ i3 + 1 ] = normals[ i ].y 130 | normalBuffer[ i3 + 2 ] = normals[ i ].z 131 | } 132 | 133 | if (hasUV) { 134 | i2 = i * 2 135 | uvBuffer[ i2 ] = uvs[ i ].x 136 | uvBuffer[ i2 + 1 ] = uvs[ i ].y 137 | } 138 | } 139 | 140 | bufferGeometry.addAttribute('position', new BufferAttribute(positionBuffer, 3)) 141 | if (hasNormal) bufferGeometry.addAttribute('normal', new BufferAttribute(normalBuffer, 3)) 142 | if (hasUV) bufferGeometry.addAttribute('uv', new BufferAttribute(uvBuffer, 2)) 143 | bufferGeometry.setIndex(new BufferAttribute(new Uint32Array(indices), 1)) 144 | return bufferGeometry 145 | 146 | function exists (v, vertices) { 147 | for (let i = 0; i < vertices.length; i++) { 148 | if (v.equals(vertices[ i ])) return i 149 | } 150 | return -1 151 | } 152 | } 153 | 154 | export { getSize, getCenter, toIndexed } 155 | --------------------------------------------------------------------------------