├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── src ├── css │ ├── app.scss │ ├── app │ │ ├── _base.scss │ │ └── _main.scss │ ├── components │ │ └── _loading.scss │ └── utils │ │ └── _normalize.scss ├── js │ ├── app.js │ ├── app │ │ ├── components │ │ │ ├── camera.js │ │ │ ├── controls.js │ │ │ ├── light.js │ │ │ ├── pathdrawer.js │ │ │ ├── renderer.js │ │ │ ├── soundobject.js │ │ │ ├── soundtrajectory.js │ │ │ └── soundzone.js │ │ ├── helpers │ │ │ ├── animation.js │ │ │ ├── geometry.js │ │ │ ├── material.js │ │ │ └── meshHelper.js │ │ ├── main.js │ │ ├── managers │ │ │ ├── datGUI.js │ │ │ ├── guiwindow.js │ │ │ └── interaction.js │ │ └── model │ │ │ ├── head.obj │ │ │ ├── model.js │ │ │ └── texture.js │ ├── data │ │ └── config.js │ └── utils │ │ ├── detector.js │ │ ├── helpers.js │ │ ├── keyboard.js │ │ ├── objloader.js │ │ └── orbitControls.js └── public │ ├── assets │ ├── css │ │ ├── app.css │ │ └── rStats.css │ ├── js │ │ ├── dat.gui.min.js │ │ ├── deflate.js │ │ ├── inflate.js │ │ ├── libtess.min.js │ │ ├── rStats.extras.js │ │ ├── rStats.js │ │ ├── simplify3D.js │ │ ├── z-worker.js │ │ └── zip.js │ ├── models │ │ ├── github.png │ │ ├── head.obj │ │ ├── head_old.obj │ │ ├── load.svg │ │ ├── mute.svg │ │ ├── save.svg │ │ └── unmute.svg │ └── sounds │ │ ├── ambience1.wav │ │ ├── ambience2.wav │ │ ├── beat1.wav │ │ ├── beat2.wav │ │ ├── chirps.wav │ │ ├── percussion.wav │ │ ├── speech1.wav │ │ ├── speech2.wav │ │ ├── synth1.wav │ │ └── synth2.wav │ └── index.html ├── webpack.config.js └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "airbnb-base", 3 | "plugins": [ 4 | "import" 5 | ] 6 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .DS_Store 4 | .idea 5 | Iconr 6 | build 7 | src/public/assets/js/app.js 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-present Anıl Çamcı 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 | # Inviso 2 | 3 | A cross-platform tool for designing interactive virtual soundscapes.
4 | Accessible online at: inviso.cc 5 | 6 | inviso 7 | 8 | An ACM UIST paper about INVISO, including a video figure, can be found here. 9 | 10 | ## Project Structure 11 | * build - Directory for built and compressed files from the npm build script 12 | * src - Directory for all dev files 13 | * src/css - Contains all SCSS files, that are compiled to `src/public/assets/css` 14 | * src/js - All the Three.js app files, with `app.js` as entry point. Compiled to `src/public/assets/js` with webpack 15 | * src/js/app/components - Three.js components that get initialized in `main.js` 16 | * src/js/app/helpers - Classes that provide ideas on how to set up and work with defaults 17 | * src/js/app/managers - Manage complex tasks such as GUI or input 18 | * src/js/app/model - Classes that set up the model object 19 | * src/js/data - Any data to be imported into app 20 | * src/js/utils - Various helpers and vendor classes 21 | * src/public - Used by webpack-dev-server to serve content and is copied over to build folder with build command. Place external vendor files here. 22 | 23 | ## Getting started 24 | Install dependencies: 25 | 26 | ``` 27 | yarn install 28 | ``` 29 | 30 | Then run dev script: 31 | 32 | ``` 33 | yarn dev 34 | ``` 35 | 36 | Spins up a webpack dev server at localhost:8080 and keeps track of all js and sass changes to files. Only reloads automatically upon save of js files. 37 | 38 | ## Build 39 | ``` 40 | yarn build 41 | ``` 42 | 43 | Cleans existing build folder and then copies over the public folder from src. Then sets environment to production and compiles js and css into build. 44 | 45 | ## Deploy 46 | ``` 47 | yarn deploy 48 | ``` 49 | 50 | Deploys `build/public` to gh-pages branch. 51 | 52 | ## Other Yarn Scripts 53 | You can run any of these individually if you'd like with the npm run command: 54 | * prebuild - Cleans build folder and lints `src/js` 55 | * clean - Cleans build folder 56 | * lint - Runs lint on `src/js` folder and uses `.eslintrc` file in root as linting rules 57 | * webpack-server - Create webpack-dev-server with hot-module-replacement 58 | * webpack-watch - Run webpack in dev environment with watch 59 | * dev:sass - Run node-sass on `src/css` folder and output to `src/public` and watch for changes 60 | * dev:js - Run webpack in dev environment without watch 61 | * build:dir - Copy files and folders from `src/public` to `build` 62 | * build:sass - Run node-sass on `src/css` and output compressed css to `build` folder 63 | * build:js - Run webpack in production environment 64 | 65 | ## Input Controls 66 | * Arrow controls will pan 67 | * Mouse left click will rotate/right click will pan 68 | * Scrollwheel zooms in and out 69 | * Delete objects with 'delete' or 'backspace' 70 | * Move dummyhead with 'w/a/s/d' 71 | * Hide stats with 'h' -- dev mode only 72 | 73 | ## Team 74 | Project leader, primary developer: Anıl Çamcı []
75 | Contributors: Kristine Lee [] (DevOps), Cody J. Roberts [] (DevOps), Angus Forbes [] 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Inviso", 3 | "version": "1.0.0", 4 | "description": "", 5 | "author": "CreativeCodingLab", 6 | "main": "app.js", 7 | "scripts": { 8 | "dev": "run-p dev:sass webpack-server webpack-watch", 9 | "build": "run-s clean build:dir build:js build:sass", 10 | "clean": "rimraf build", 11 | "deploy": "gh-pages -d build/public", 12 | "lint": "eslint src/js/", 13 | "webpack-server": "set NODE_ENV=0&& webpack-dev-server --progress --colors --hot --inline --open", 14 | "webpack-watch": "set NODE_ENV=0&& webpack --progress --colors --watch --cache", 15 | "dev:sass": "node-sass -w -r src/css/ -o src/public/assets/css/", 16 | "dev:js": "set NODE_ENV=0&& webpack", 17 | "build:dir": "copyfiles -u 1 \"src/public/**/*\" build/", 18 | "build:sass": "node-sass --output-style compressed src/css/ -o build/public/assets/css/", 19 | "build:js": "set NODE_ENV=1&& webpack" 20 | }, 21 | "dependencies": { 22 | "es6-promise": "^4.0.5", 23 | "sass-loader": "^6.0.6", 24 | "three": "^0.87.0", 25 | "three-obj-loader": "^1.1.0", 26 | "tween.js": "16.6.0", 27 | "whatwg-fetch": "^2.0.3" 28 | }, 29 | "devDependencies": { 30 | "gh-pages": "^0.12.0", 31 | "babel-core": "^6.17.0", 32 | "babel-loader": "^6.2.5", 33 | "babel-preset-es2015": "^6.16.0", 34 | "copyfiles": "^1.2.0", 35 | "eslint": "^3.13.0", 36 | "eslint-config-airbnb-base": "^11.0.0", 37 | "eslint-plugin-import": "^2.2.0", 38 | "node-sass": "^3.10.1", 39 | "npm-run-all": "^3.0.0", 40 | "rimraf": "^2.6.1", 41 | "webpack": "^1.13.2", 42 | "webpack-dev-middleware": "^1.8.3", 43 | "webpack-dev-server": "^1.16.2" 44 | }, 45 | "engines": { 46 | "node": "8.3.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/css/app.scss: -------------------------------------------------------------------------------- 1 | // Utils 2 | @import './utils/normalize'; 3 | 4 | // App 5 | @import './app/base'; 6 | @import './app/main'; 7 | 8 | // Components 9 | @import './components/loading'; 10 | 11 | #UIContainer { 12 | position: absolute; 13 | right: 200px; 14 | top: 0%; 15 | column-count: 1; 16 | } 17 | 18 | #add-object-button { 19 | z-index: 1; 20 | position: relative; 21 | margin: 0; 22 | margin-top: -2px; 23 | float: left; 24 | color: white; 25 | border: 0px; 26 | display: inline-block; 27 | padding: 0px 10px; 28 | padding-bottom: 4px; 29 | cursor: pointer; 30 | background-color: #1177ff; 31 | font-size: 30px; 32 | } 33 | #add-object-button.active { 34 | background-color: #f44b76; 35 | } 36 | 37 | #guis { 38 | position: absolute; 39 | top: 40px; 40 | width: 220px; 41 | background-color: rgba(255, 255, 255, 0.6); 42 | font-family: roboto, 'helvetica neue', helvetica, arial, sans-serif; 43 | line-height: 20px; 44 | box-shadow: 0 0 5px #aaa; 45 | transition: background-color 200ms ease, color 200ms ease, opacity 200ms ease; 46 | -webkit-transition: background-color 200ms ease, color 200ms ease, 47 | opacity 200ms ease; 48 | } 49 | 50 | #guis > div { 51 | padding: 15px; 52 | position: relative; 53 | } 54 | 55 | #guis > div + div { 56 | border-top: 0.5px solid #aaa; 57 | } 58 | 59 | #guis h4 { 60 | position: relative; 61 | margin: 0; 62 | margin-top: 0.2em; 63 | font-weight: 400; 64 | text-align: center; 65 | } 66 | 67 | #guis h4 + * { 68 | margin-top: 1em; 69 | } 70 | 71 | #guis div > span { 72 | font-size: 0.8em; 73 | vertical-align: top; 74 | } 75 | 76 | #guis .property { 77 | font-weight: bold; 78 | display: inline-block; 79 | width: 70px; 80 | position: absolute; 81 | } 82 | 83 | #guis .valueSpan { 84 | cursor: pointer; 85 | border-bottom: 1px dotted #555; 86 | display: inline-block; 87 | 88 | max-width: 90px; 89 | overflow: hidden; 90 | text-overflow: ellipsis; 91 | } 92 | 93 | #guis { 94 | -webkit-touch-callout: none; /* iOS Safari */ 95 | -webkit-user-select: none; /* Chrome/Safari/Opera */ 96 | -khtml-user-select: none; /* Konqueror */ 97 | -moz-user-select: none; /* Firefox */ 98 | -ms-user-select: none; /* Internet Explorer/Edge */ 99 | user-select: none; /* Non-prefixed version, currently 100 | not supported by any browser */ 101 | } 102 | 103 | .property + .valueSpan { 104 | margin-left: 80px; 105 | } 106 | 107 | #guis.editor { 108 | background-color: #555; 109 | color: white; 110 | } 111 | 112 | #guis.editor .valueSpan { 113 | border-bottom-color: white; 114 | } 115 | 116 | #guis .edit-toggle { 117 | line-height: 1.2em; 118 | color: indigo; 119 | } 120 | 121 | #guis.editor .edit-toggle { 122 | color: skyblue; 123 | } 124 | 125 | #guis .remove-file { 126 | padding: 0 2px; 127 | margin: 0 2px; 128 | color: #f44b76; 129 | cursor: pointer; 130 | font-size: 20px; 131 | line-height: 11px; 132 | display: inline-block; 133 | vertical-align: top; 134 | font-weight: bold; 135 | } 136 | 137 | #guis .remove-file:hover { 138 | color: pink; 139 | } 140 | 141 | #guis .cone, 142 | #guis.editor .cone { 143 | color: black; 144 | } 145 | #guis .cone .valueSpan, 146 | #guis.editor .cone .valueSpan { 147 | border-bottom-color: #555; 148 | } 149 | 150 | #guis.editor .nav-object { 151 | color: rgba(220, 220, 220, 0.5); 152 | } 153 | #guis.editor .nav-object:hover { 154 | color: white; 155 | } 156 | #guis .nav { 157 | color: rgba(85, 85, 85, 0.5); 158 | transition: color 100ms ease; 159 | 160 | top: 10px; 161 | cursor: pointer; 162 | padding: 0 12px; 163 | font-size: 40px; 164 | position: absolute; 165 | line-height: 1.1em; 166 | } 167 | #guis .nav:hover { 168 | color: #333; 169 | text-shadow: 0 0 5px #aaa; 170 | } 171 | #guis .nav-left { 172 | left: 0; 173 | } 174 | #guis .nav-right { 175 | right: 0; 176 | } 177 | 178 | #guis .active { 179 | background-color: #99ccff; 180 | } 181 | 182 | input[type='file'] { 183 | display: none; 184 | } 185 | 186 | #camera-label { 187 | z-index: 1; 188 | position: absolute; 189 | bottom: 25px; 190 | width: 100%; 191 | text-align: center; 192 | font-size: 18px; 193 | text-decoration: underline; 194 | text-decoration-style: dotted; 195 | -moz-text-decoration-style: dotted; 196 | -webkit-text-decoration-style: dotted; 197 | text-decoration-style: dotted; 198 | color: #555; 199 | cursor: pointer; 200 | } 201 | 202 | #play-mute-button { 203 | position: absolute; 204 | top: 10px; 205 | left: 15px; 206 | } 207 | #play-mute-button img { 208 | cursor: pointer; 209 | width: 20px; 210 | height: 20px; 211 | opacity: 0.5; 212 | } 213 | #play-mute-button img:hover { 214 | opacity: 0.66; 215 | } 216 | #unmute-button { 217 | display: none; 218 | } 219 | 220 | #save { 221 | position: absolute; 222 | bottom: 10px; 223 | left: 15px; 224 | } 225 | 226 | #save img { 227 | cursor: pointer; 228 | width: 15px; 229 | height: 15px; 230 | opacity: 0.5; 231 | } 232 | 233 | #load { 234 | z-index: 2; 235 | position: absolute; 236 | bottom: 10px; 237 | left: 45px; 238 | } 239 | 240 | #load img { 241 | cursor: pointer; 242 | width: 15px; 243 | height: 15px; 244 | opacity: 0.67; 245 | } 246 | 247 | #githubLink { 248 | z-index: 2; 249 | position: absolute; 250 | bottom: 10px; 251 | right: 15px; 252 | width: 25px; 253 | height: 25px; 254 | opacity: 0.7; 255 | } 256 | 257 | .help-bubble { 258 | float: left; 259 | position: absolute; 260 | padding: 10px; 261 | color: #fff; 262 | background: #777; 263 | opacity: 0.75; 264 | -webkit-border-radius: 10px; 265 | -moz-border-radius: 10px; 266 | border-radius: 10px; 267 | display: block; 268 | font-size: 12px; 269 | font-family: roboto, 'helvetica neue', helvetica, arial, sans-serif; 270 | line-height: 1.25em; 271 | pointer-events: none; 272 | } 273 | -------------------------------------------------------------------------------- /src/css/app/_base.scss: -------------------------------------------------------------------------------- 1 | body { 2 | overflow: hidden; 3 | } 4 | -------------------------------------------------------------------------------- /src/css/app/_main.scss: -------------------------------------------------------------------------------- 1 | .main { 2 | position: relative; 3 | width: 100%; 4 | height: 100vh; 5 | } 6 | -------------------------------------------------------------------------------- /src/css/components/_loading.scss: -------------------------------------------------------------------------------- 1 | #loading { 2 | font-family:roboto,'helvetica neue', helvetica, arial, sans-serif; 3 | position: absolute; 4 | top: calc(50% - 20px); 5 | left: calc(50% - 35px); 6 | } 7 | -------------------------------------------------------------------------------- /src/css/utils/_normalize.scss: -------------------------------------------------------------------------------- 1 | html { 2 | -ms-text-size-adjust: 100%; 3 | -webkit-text-size-adjust: 100%; 4 | } 5 | body { 6 | margin: 0; 7 | font: 16px/1 sans-serif; 8 | -moz-osx-font-smoothing: grayscale; 9 | -webkit-font-smoothing: antialiased; 10 | } 11 | h1, 12 | h2, 13 | h3, 14 | h4, 15 | p, 16 | blockquote, 17 | figure, 18 | ol, 19 | ul { 20 | margin: 0; 21 | padding: 0; 22 | } 23 | main, 24 | li { 25 | display: block; 26 | } 27 | h1, 28 | h2, 29 | h3, 30 | h4 { 31 | font-size: inherit; 32 | } 33 | strong { 34 | font-weight: bold; 35 | } 36 | a, 37 | button { 38 | color: inherit; 39 | transition: .3s; 40 | } 41 | a { 42 | text-decoration: none; 43 | } 44 | button { 45 | overflow: visible; 46 | border: 0; 47 | font: inherit; 48 | -webkit-font-smoothing: inherit; 49 | letter-spacing: inherit; 50 | background: none; 51 | cursor: pointer; 52 | } 53 | ::-moz-focus-inner { 54 | padding: 0; 55 | border: 0; 56 | } 57 | :focus { 58 | outline: 0; 59 | } 60 | img { 61 | max-width: 100%; 62 | height: auto; 63 | border: 0; 64 | } 65 | -------------------------------------------------------------------------------- /src/js/app.js: -------------------------------------------------------------------------------- 1 | import Config from './data/config'; 2 | import Detector from './utils/detector'; 3 | import Main from './app/main'; 4 | 5 | // Check environment and set the Config helper 6 | if (__ENV__ == 'dev') { 7 | console.log('----- RUNNING IN DEV ENVIRONMENT! -----'); 8 | 9 | Config.isDev = true; 10 | } 11 | 12 | function init() { 13 | // Check for webGL capabilities 14 | if (!Detector.webgl) { 15 | Detector.addGetWebGLMessage(); 16 | } else { 17 | const container = document.getElementById('appContainer'); 18 | new Main(container); 19 | } 20 | } 21 | 22 | init(); 23 | -------------------------------------------------------------------------------- /src/js/app/components/camera.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | import Config from '../../data/config'; 4 | 5 | // Class that creates and updates the main camera 6 | export default class Camera { 7 | constructor(renderer) { 8 | const width = renderer.domElement.width; 9 | const height = renderer.domElement.height; 10 | 11 | // Create and position a Perspective Camera 12 | this.threeCamera = new THREE.PerspectiveCamera(Config.camera.fov, width / height, Config.camera.near, Config.camera.far); 13 | this.threeCamera.position.set(Config.camera.posX, Config.camera.posY, Config.camera.posZ); 14 | 15 | // Initial sizing 16 | this.updateSize(renderer); 17 | 18 | // Listeners 19 | window.addEventListener('resize', () => this.updateSize(renderer), false); 20 | } 21 | 22 | updateSize(renderer) { 23 | // Multiply by dpr in case it is retina device 24 | this.threeCamera.aspect = renderer.domElement.width / renderer.domElement.height; 25 | 26 | // Always call updateProjectionMatrix on camera change 27 | this.threeCamera.updateProjectionMatrix(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/js/app/components/controls.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | import OrbitControls from '../../utils/orbitControls'; 4 | import Config from '../../data/config'; 5 | 6 | // Controls based on orbit controls 7 | export default class Controls { 8 | constructor(camera, container) { 9 | // Orbit controls first needs to pass in THREE to constructor 10 | const orbitControls = new OrbitControls(THREE); 11 | this.threeControls = new orbitControls(camera, container); 12 | 13 | // Controls are only in enabled via 'c' keypress 14 | this.threeControls.enabled = true; 15 | 16 | this.threeControls.mouseButtons = { 17 | ORBIT: THREE.MOUSE.LEFT, 18 | PAN: THREE.MOUSE.RIGHT, 19 | ZOOM: THREE.MOUSE.MIDDLE 20 | } 21 | 22 | this.init(); 23 | } 24 | 25 | disableZoom() { 26 | this.threeControls.enableZoom = false; 27 | } 28 | enableZoom() { 29 | this.threeControls.enableZoom = true; 30 | } 31 | 32 | disablePan() { 33 | this.threeControls.enablePan = false; 34 | } 35 | enablePan() { 36 | this.threeControls.enablePan = true; 37 | } 38 | 39 | disable() { 40 | this.threeControls.enabled = false; 41 | } 42 | enable() { 43 | this.threeControls.enabled = true; 44 | } 45 | 46 | init() { 47 | this.threeControls.target.set(Config.controls.target.x, Config.controls.target.y, Config.controls.target.z); 48 | this.threeControls.autoRotate = Config.controls.autoRotate; 49 | this.threeControls.autoRotateSpeed = Config.controls.autoRotateSpeed; 50 | this.threeControls.rotateSpeed = Config.controls.rotateSpeed; 51 | this.threeControls.zoomSpeed = Config.controls.zoomSpeed; 52 | this.threeControls.minDistance = Config.controls.minDistance; 53 | this.threeControls.maxDistance = Config.controls.maxDistance; 54 | this.threeControls.minPolarAngle = Config.controls.minPolarAngle; 55 | this.threeControls.maxPolarAngle = Config.controls.maxPolarAngle; 56 | this.threeControls.enableDamping = Config.controls.enableDamping; 57 | this.threeControls.enableZoom = Config.controls.enableZoom; 58 | this.threeControls.dampingFactor = Config.controls.dampingFactor; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/js/app/components/light.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | import Config from '../../data/config'; 4 | 5 | // Sets up and places all lights in scene 6 | export default class Light { 7 | constructor(scene) { 8 | this.scene = scene; 9 | 10 | this.init(); 11 | } 12 | 13 | init() { 14 | // Ambient 15 | this.ambientLight = new THREE.AmbientLight(Config.ambientLight.color); 16 | this.ambientLight.visible = Config.ambientLight.enabled; 17 | 18 | // Point light 19 | this.pointLight = new THREE.PointLight(Config.pointLight.color, Config.pointLight.intensity, Config.pointLight.distance); 20 | this.pointLight.position.set(Config.pointLight.x, Config.pointLight.y, Config.pointLight.z); 21 | this.pointLight.visible = Config.pointLight.enabled; 22 | 23 | // Directional light 24 | this.directionalLight = new THREE.DirectionalLight(Config.directionalLight.color, Config.directionalLight.intensity); 25 | this.directionalLight.position.set(Config.directionalLight.x, Config.directionalLight.y, Config.directionalLight.z); 26 | this.directionalLight.position.multiplyScalar(Config.directionalLight.multiplyScalar); 27 | this.directionalLight.visible = Config.directionalLight.enabled; 28 | this.directionalLight.color.setHSL(Config.directionalLight.hue, Config.directionalLight.saturation, Config.directionalLight.lightness); 29 | 30 | // Shadow map 31 | this.directionalLight.castShadow = Config.shadow.enabled; 32 | this.directionalLight.shadow.bias = Config.shadow.bias; 33 | this.directionalLight.shadow.camera.near = Config.shadow.near; 34 | this.directionalLight.shadow.camera.far = Config.shadow.far; 35 | this.directionalLight.shadow.camera.left = Config.shadow.left; 36 | this.directionalLight.shadow.camera.right = Config.shadow.right; 37 | this.directionalLight.shadow.camera.top = Config.shadow.top; 38 | this.directionalLight.shadow.camera.bottom = Config.shadow.bottom; 39 | this.directionalLight.shadow.mapSize.width = Config.shadow.mapWidth; 40 | this.directionalLight.shadow.mapSize.height = Config.shadow.mapHeight; 41 | 42 | // Shadow camera helper 43 | this.directionalLightHelper = new THREE.CameraHelper(this.directionalLight.shadow.camera); 44 | this.directionalLightHelper.visible = Config.shadow.helperEnabled; 45 | 46 | // Hemisphere light 47 | this.hemiLight = new THREE.HemisphereLight(Config.hemiLight.color, Config.hemiLight.groundColor, Config.hemiLight.intensity); 48 | this.hemiLight.position.set(Config.hemiLight.x, Config.hemiLight.y, Config.hemiLight.z); 49 | this.hemiLight.visible = Config.hemiLight.enabled; 50 | } 51 | 52 | place(lightName) { 53 | switch(lightName) { 54 | case 'ambient': 55 | this.scene.add(this.ambientLight); 56 | break; 57 | 58 | case 'directional': 59 | this.scene.add(this.directionalLight); 60 | this.scene.add(this.directionalLightHelper); 61 | break; 62 | 63 | case 'point': 64 | this.scene.add(this.pointLight); 65 | break; 66 | 67 | case 'hemi': 68 | this.scene.add(this.hemiLight); 69 | break; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/js/app/components/pathdrawer.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import SoundObject from './soundobject'; 3 | import SoundTrajectory from './soundtrajectory'; 4 | import SoundZone from './soundzone'; 5 | 6 | export default class PathDrawer { 7 | constructor(scene) { 8 | this.parentObject = null; 9 | this.scene = scene; 10 | this.points = []; 11 | this.lines = []; 12 | this.lastPoint = new THREE.Vector3(); 13 | this.isDrawing = false, 14 | 15 | this.material = { 16 | trajectory: new THREE.LineBasicMaterial({ 17 | linewidth: 2, 18 | color: 0x999999 19 | }), 20 | zone: new THREE.LineBasicMaterial({ 21 | color: 0xff1169 22 | }) 23 | }; 24 | } 25 | 26 | beginAt(point, trajectoryContainerObject) { 27 | this.isDrawing = true; 28 | this.parentObject = trajectoryContainerObject || null; 29 | this.lastPoint = point; 30 | this.points = [point]; 31 | } 32 | 33 | addPoint(point) { 34 | if (this.isDrawing) { // redundant check? just to be safe for now 35 | const material = this.parentObject 36 | ? this.material.trajectory 37 | : this.material.zone; 38 | const geometry = new THREE.Geometry(); 39 | geometry.vertices.push( this.lastPoint, point ); 40 | 41 | const line = new THREE.Line( geometry, material ); 42 | 43 | this.lastPoint = point; 44 | this.points.push(point); 45 | this.lines.push(line); 46 | this.scene.add(line); 47 | } 48 | } 49 | 50 | createObject(main, loader = false) { 51 | if (this.isDrawing || loader) { 52 | this.isDrawing = false; 53 | const points = simplify(this.points, 10, true); 54 | let object; 55 | if (this.parentObject) { 56 | if (points.length >= 2) { 57 | object = new SoundTrajectory(main, points); 58 | if (main.isUserStudyLoading) object.turnInvisible(); 59 | object.points = points; 60 | this.parentObject.trajectory = object; 61 | object.parentSoundObject = this.parentObject; 62 | main.soundTrajectories.push(object); 63 | } 64 | } 65 | else { 66 | if (points.length >= 3) { 67 | object = new SoundZone(main, points); 68 | if (main.isUserStudyLoading) object.turnInvisible(); 69 | main.soundZones.push(object); 70 | } else { 71 | object = new SoundObject(main); 72 | if (main.isUserStudyLoading) object.turnInvisible(); 73 | main.soundObjects.push(object); 74 | } 75 | } 76 | 77 | this.clear(); 78 | 79 | if (object) { 80 | object.addToScene(this.scene); 81 | } 82 | return object; 83 | } 84 | else { 85 | console.log('called createObject when not drawing') 86 | } 87 | } 88 | 89 | clear() { 90 | this.parentObject = null; 91 | this.lines.forEach((line) => { 92 | this.scene.remove(line); 93 | }); 94 | this.lines = []; 95 | this.points = []; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/js/app/components/renderer.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | import Config from '../../data/config'; 4 | 5 | // Main webGL renderer class 6 | export default class Renderer { 7 | constructor(scene, container) { 8 | // Properties 9 | this.scene = scene; 10 | this.container = container; 11 | 12 | // Create WebGL renderer and set its antialias 13 | this.threeRenderer = new THREE.WebGLRenderer({antialias: true}); 14 | 15 | // Set clear color to fog to enable fog or to hex color for no fog 16 | this.threeRenderer.setClearColor(0xf0f0f0); 17 | this.threeRenderer.setPixelRatio(window.devicePixelRatio); // For retina 18 | 19 | this.threeRenderer.autoClear = false; 20 | 21 | // Appends canvas 22 | container.appendChild(this.threeRenderer.domElement); 23 | 24 | // Shadow map options 25 | this.threeRenderer.shadowMap.enabled = true; 26 | // this.threeRenderer.shadowMap.type = THREE.PCFSoftShadowMap; 27 | 28 | // Get anisotropy for textures 29 | Config.maxAnisotropy = this.threeRenderer.getMaxAnisotropy(); 30 | 31 | // Initial size update set to canvas container 32 | this.updateSize(); 33 | 34 | // Listeners 35 | document.addEventListener('DOMContentLoaded', () => this.updateSize(), false); 36 | window.addEventListener('resize', () => this.updateSize(), false); 37 | } 38 | 39 | updateSize() { 40 | this.threeRenderer.setSize(this.container.offsetWidth, this.container.offsetHeight); 41 | } 42 | 43 | render(scene, camera) { 44 | // Renders scene to canvas target 45 | this.threeRenderer.clear(); 46 | this.threeRenderer.setViewport( 0, 0, window.innerWidth, window.innerHeight ); 47 | this.threeRenderer.render(scene, camera); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/js/app/components/soundobject.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import 'whatwg-fetch'; 3 | 4 | import Config from '../../data/config'; 5 | import Helpers from '../../utils/helpers'; 6 | 7 | export default class SoundObject { 8 | constructor(main) { 9 | this.type = 'SoundObject'; 10 | this.posX = 0; 11 | this.posY = 0; 12 | this.posZ = 0; 13 | this.radius = Config.soundObject.defaultRadius; 14 | this.cones = []; 15 | this.audio = main.audio; 16 | this.gui = main.gui; 17 | 18 | this.trajectory = null; 19 | this.trajectoryClock = Config.soundObject.defaultTrajectoryClock; 20 | this.movementSpeed = Config.soundObject.defaultMovementSpeed; 21 | this.movementDirection = Config.soundObject.defaultMovementDirection; 22 | this.movementIncrement = null; 23 | 24 | this.containerObject = new THREE.Object3D(); 25 | 26 | const sphereGeometry = new THREE.SphereBufferGeometry(this.radius, 100, 100); 27 | const sphereMaterial = new THREE.MeshBasicMaterial({ 28 | color: 0xFFFFFF, 29 | opacity: 0.8, 30 | transparent: true, 31 | premultipliedAlpha: true 32 | }); 33 | this.omniSphere = new THREE.Mesh(sphereGeometry, sphereMaterial); 34 | this.omniSphere.name = 'omniSphere'; 35 | this.omniSphere.castShadow = true; 36 | 37 | const raycastSphereGeometry = new THREE.SphereBufferGeometry(150, 100, 100); 38 | const raycastSphereMaterial = new THREE.MeshBasicMaterial({ color: 0xFFFFFF, visible: false }); 39 | this.raycastSphere = new THREE.Mesh(raycastSphereGeometry, raycastSphereMaterial); 40 | this.raycastSphere.name = 'sphere'; 41 | this.raycastSphere.position.copy(main.mouse); 42 | main.scene.add(this.raycastSphere); 43 | 44 | this.axisHelper = new THREE.AxisHelper(100); 45 | this.axisHelper.position.copy(main.mouse); 46 | main.scene.add(this.axisHelper); 47 | 48 | const lineMaterial = new THREE.LineDashedMaterial({ 49 | color: 0x888888, 50 | dashSize: 30, 51 | gapSize: 30, 52 | }); 53 | const lineGeometry = new THREE.Geometry(); 54 | 55 | lineGeometry.vertices.push( 56 | new THREE.Vector3(0, 0, -300), 57 | new THREE.Vector3(0, 0, 300), 58 | ); 59 | 60 | lineGeometry.computeLineDistances(); 61 | this.altitudeHelper = new THREE.Line(lineGeometry, lineMaterial); 62 | this.altitudeHelper.rotation.x = Math.PI / 2; 63 | main.scene.add(this.altitudeHelper); 64 | this.altitudeHelper.position.copy(main.mouse); 65 | 66 | this.containerObject.add(this.omniSphere); 67 | this.containerObject.position.copy(main.mouse); 68 | main.scene.add(this.containerObject); 69 | } 70 | 71 | createCone(sound, color = null) { 72 | sound.volume.gain.value = 1; 73 | sound.spread = 0.5; 74 | 75 | const coneWidth = sound.spread * 90; 76 | const coneHeight = sound.volume.gain.value * 50 + 50; 77 | 78 | const coneGeo = new THREE.CylinderGeometry(coneWidth, 0, coneHeight, 100, 1, true); 79 | const randGreen = color !== null ? color : Math.random(); 80 | const randBlue = color !== null ? color : Math.random(); 81 | //const coneColor = new THREE.Color(0.5, randGreen, randBlue); 82 | const coneColor = new THREE.Color(); 83 | coneColor.setHSL(randGreen, randBlue, 0.8); 84 | const coneMaterial = new THREE.MeshBasicMaterial({ 85 | color: coneColor, 86 | opacity: 0.8, 87 | transparent:true 88 | }); 89 | 90 | coneGeo.translate(0, coneHeight / 2, 0); 91 | coneGeo.rotateX(Math.PI / 2); 92 | coneMaterial.side = THREE.DoubleSide; 93 | 94 | const cone = new THREE.Mesh(coneGeo, coneMaterial); 95 | 96 | cone.randGreen = randGreen; 97 | cone.sound = sound; 98 | cone.sound.panner.coneInnerAngle = Math.atan(coneWidth / coneHeight) * (180 / Math.PI); 99 | cone.sound.panner.coneOuterAngle = cone.sound.panner.coneInnerAngle * 3; 100 | cone.sound.panner.coneOuterGain = 0.05; 101 | // cone.sound.volume.gain.value = Helpers.mapRange(coneHeight, 100, 150, 0.5, 2); 102 | 103 | cone.name = 'cone'; 104 | cone.baseColor = coneColor; 105 | cone.hoverColor = function() { 106 | let c = this.baseColor.clone(); 107 | c.offsetHSL(0,-0.05,0.1); 108 | return c; 109 | } 110 | 111 | sound.scriptNode.onaudioprocess = function() { 112 | let array = new Uint8Array(sound.analyser.frequencyBinCount); 113 | sound.analyser.getByteFrequencyData(array); 114 | let values = 0; 115 | let length = array.length; 116 | for (let i = 0; i < length; i++) values += array[i]; 117 | let average = values / length; 118 | cone.material.opacity = Helpers.mapRange(average, 50, 100, 0.65, 0.95); 119 | } 120 | 121 | cone.long = cone.lat = 0; 122 | 123 | this.cones.push(cone); 124 | this.containerObject.add(cone); 125 | this.setAudioPosition(cone); 126 | return cone; 127 | } 128 | 129 | setAudioPosition(object) { 130 | const o = new THREE.Vector3(); 131 | object.updateMatrixWorld(); 132 | o.setFromMatrixPosition(object.matrixWorld); 133 | object.sound.panner.setPosition(o.x, o.y, o.z); 134 | 135 | if (object.name == 'cone') { 136 | const p = new THREE.Vector3(); 137 | const q = new THREE.Vector3(); 138 | const m = object.matrixWorld; 139 | 140 | const mx = m.elements[12]; 141 | const my = m.elements[13]; 142 | const mz = m.elements[14]; 143 | 144 | const vec = new THREE.Vector3(0, 0, 1); 145 | 146 | m.elements[12] = m.elements[13] = m.elements[14] = 0; 147 | 148 | vec.applyMatrix4(m); 149 | vec.normalize(); 150 | object.sound.panner.setOrientation(vec.x, vec.y, vec.z); 151 | 152 | m.elements[12] = mx; 153 | m.elements[13] = my; 154 | m.elements[14] = mz; 155 | } 156 | } 157 | 158 | loadSound(file, audio, mute, object) { 159 | const context = audio.context; 160 | const mainMixer = context.createGain(); 161 | let reader = new FileReader(); 162 | var sound = {}; 163 | 164 | 165 | if (object) { // cones can be null at this point 166 | object.filename = file.name; 167 | object.file = file; 168 | } 169 | 170 | let promise = new Promise(function(resolve, reject) { 171 | reader.onload = (ev) => { 172 | context.decodeAudioData(ev.target.result, function(decodedData) { 173 | if (object && object.type === 'SoundObject') { 174 | /* attach omnidirectional sound */ 175 | object = object.omniSphere; 176 | } 177 | 178 | if (object && object.sound) { 179 | object.sound.source.stop(); 180 | object.sound.source.disconnect(object.sound.scriptNode); 181 | object.sound.scriptNode.disconnect(context.destination); 182 | } 183 | 184 | sound.mainMixer = mainMixer; 185 | 186 | sound.analyser = context.createAnalyser(); 187 | sound.analyser.smoothingTimeConstant = 0.5; 188 | sound.analyser.fftSize = 1024; 189 | 190 | sound.scriptNode = context.createScriptProcessor(2048, 1, 1); 191 | sound.scriptNode.connect(context.destination); 192 | 193 | sound.source = context.createBufferSource(); 194 | sound.source.loop = true; 195 | sound.source.connect(sound.scriptNode); 196 | 197 | sound.panner = context.createPanner(); 198 | sound.panner.panningModel = 'HRTF'; 199 | sound.panner.distanceModel = 'inverse'; 200 | sound.panner.refDistance = 100; 201 | 202 | // sound.panner.rolloffFactor = 5; 203 | 204 | sound.volume = context.createGain(); 205 | sound.source.connect(sound.volume); 206 | sound.volume.connect(sound.analyser); 207 | sound.volume.connect(sound.panner); 208 | sound.panner.connect(mainMixer); 209 | mainMixer.connect(audio.destination); 210 | mainMixer.gain.value = mute ? 0 : 1; 211 | 212 | sound.source.buffer = decodedData; 213 | sound.source.start(context.currentTime + 0.020); 214 | 215 | if (object && object.name === 'omniSphere') { 216 | sound.scriptNode.onaudioprocess = () => { 217 | const array = new Uint8Array(sound.analyser.frequencyBinCount); 218 | sound.analyser.getByteFrequencyData(array); 219 | let values = 0; 220 | const length = array.length; 221 | for (let i = 0; i < length; i++) values += array[i]; 222 | const average = values / length; 223 | object.material.opacity = Helpers.mapRange(average, 50, 100, 0.65, 0.95); 224 | }; 225 | } 226 | 227 | resolve(sound); 228 | }); 229 | }; 230 | 231 | reader.readAsArrayBuffer(file); 232 | }); 233 | 234 | return promise; 235 | } 236 | 237 | isUnderMouse(ray) { 238 | return ray.intersectObject(this.containerObject, true).length > 0; 239 | } 240 | 241 | select(main) { 242 | this.nonScaledMouseOffsetY = main.nonScaledMouse.y; 243 | } 244 | 245 | move(main) { 246 | let pointer; 247 | 248 | if (main.perspectiveView) { 249 | const posY = Helpers.mapRange( 250 | main.nonScaledMouse.y - this.nonScaledMouseOffsetY, 251 | -0.5, 252 | 0.5, 253 | -200, 254 | 200, 255 | ); 256 | 257 | pointer = this.containerObject.position; 258 | if (pointer.y > -200 || pointer.y < 200) pointer.y += posY; 259 | 260 | // clamp 261 | pointer.y = Math.max(Math.min(pointer.y, 300), -300); 262 | 263 | this.nonScaledMouseOffsetY = main.nonScaledMouse.y; 264 | } else { 265 | pointer = main.mouse; 266 | pointer.y = this.containerObject.position.y; 267 | } 268 | 269 | if (this.trajectory) this.trajectory.move(pointer, main.nonScaledMouse, main.perspectiveView); 270 | 271 | this.setPosition(pointer); 272 | } 273 | 274 | setPosition(position) { 275 | this.containerObject.position.copy(position); 276 | this.axisHelper.position.copy(position); 277 | this.altitudeHelper.position.copy(position); 278 | this.altitudeHelper.position.y = 0; 279 | this.raycastSphere.position.copy(position); 280 | 281 | if (this.cones[0]) { 282 | for (const i in this.cones) { 283 | this.setAudioPosition(this.cones[i]); 284 | } 285 | } 286 | 287 | if (this.omniSphere.sound){ 288 | this.setAudioPosition(this.omniSphere); 289 | } 290 | } 291 | 292 | addToScene(scene) { 293 | scene.add(this.containerObject); 294 | } 295 | 296 | setActive(main) { 297 | if (this.trajectory) { 298 | this.trajectory.setActive(); 299 | this.trajectory.setMouseOffset(main.nonScaledMouse, main.mouse); 300 | } 301 | } 302 | 303 | setInactive() { 304 | if (this.trajectory) { 305 | this.trajectory.setInactive(); 306 | } 307 | } 308 | 309 | changeRadius() { 310 | if (this.omniSphere.sound && this.omniSphere.sound.volume) { 311 | const r = 0.5 + 0.5*this.omniSphere.sound.volume.gain.value; 312 | this.omniSphere.scale.x = this.omniSphere.scale.y = this.omniSphere.scale.z = r; 313 | } 314 | else { 315 | this.omniSphere.scale.x = this.omniSphere.scale.y = this.omniSphere.scale.z = 1; 316 | } 317 | } 318 | 319 | changeLength(cone) { 320 | const r = cone.sound.spread * 90; 321 | const l = cone.sound.volume.gain.value * 50 + 50; 322 | cone.sound.panner.coneInnerAngle = Math.atan( r / l) * (180 / Math.PI); 323 | cone.sound.panner.coneOuterAngle = cone.sound.panner.coneInnerAngle * 1.5; 324 | 325 | cone.geometry.dynamic = true; 326 | 327 | let circVertices = cone.geometry.vertices.slice(0,-1); 328 | let origin = cone.geometry.vertices[cone.geometry.vertices.length-1]; 329 | 330 | circVertices.forEach(vertex => { 331 | let v = new THREE.Vector3().subVectors(vertex, origin).normalize(); 332 | vertex.copy(origin.clone().addScaledVector(v, l)); 333 | }) 334 | 335 | cone.geometry.verticesNeedUpdate = true; 336 | } 337 | 338 | changeWidth(cone) { 339 | const r = cone.sound.spread * 90; 340 | const l = cone.sound.volume.gain.value * 50 + 50; 341 | cone.sound.panner.coneInnerAngle = Math.atan( r / l) * (180 / Math.PI); 342 | cone.sound.panner.coneOuterAngle = cone.sound.panner.coneInnerAngle * 3; 343 | 344 | cone.geometry.dynamic = true; 345 | 346 | let circVertices = cone.geometry.vertices.slice(0,-1); 347 | let center = new THREE.Vector3(); 348 | center.lerpVectors(circVertices[0], circVertices[Math.round(circVertices.length/2)], 0.5); 349 | 350 | circVertices.forEach(vertex => { 351 | let v = new THREE.Vector3().subVectors(vertex, center).normalize(); 352 | vertex.copy(center.clone().addScaledVector(v, r)); 353 | }) 354 | 355 | cone.geometry.verticesNeedUpdate = true; 356 | } 357 | 358 | pointCone(cone, point) { 359 | const coneRotation = new THREE.Vector3(); 360 | coneRotation.subVectors(point, this.containerObject.position); 361 | cone.lookAt(coneRotation); 362 | this.setAudioPosition(cone); 363 | 364 | const longlat = (function( vector3 ) { 365 | // taken from https://gist.github.com/nicoptere/2f2571db4b454bb18cd9 366 | vector3.normalize(); 367 | 368 | //longitude = angle of the vector around the Y axis 369 | //-( ) : negate to flip the longitude (3d space specific ) 370 | //- PI / 2 to face the Z axis 371 | var lng = -( Math.atan2( -vector3.z, -vector3.x ) ) - Math.PI / 2; 372 | 373 | //to bind between -PI / PI 374 | if( lng < - Math.PI )lng += Math.PI*2; 375 | 376 | //latitude : angle between the vector & the vector projected on the XZ plane on a unit sphere 377 | 378 | //project on the XZ plane 379 | var p = new THREE.Vector3( vector3.x, 0, vector3.z ); 380 | //project on the unit sphere 381 | p.normalize(); 382 | 383 | //compute the angle ( both vectors are normalized, no division by the sum of lengths ) 384 | var lat = Math.acos( p.dot( vector3 ) ); 385 | 386 | //invert if Y is negative to ensure the latitude is between -PI/2 & PI / 2 387 | if( vector3.y < 0 ) lat *= -1; 388 | 389 | return [ lng,lat ]; 390 | 391 | })( coneRotation ); 392 | cone.long = longlat[0]; 393 | cone.lat = longlat[1]; 394 | } 395 | 396 | // Needs to be refactored - also lives in guiwindow 397 | pointConeMagic(cone, lat, long) { 398 | // adapted from https://gist.github.com/nicoptere/2f2571db4b454bb18cd9 399 | const v = (function lonLatToVector3( lng, lat ) 400 | { 401 | //flips the Y axis 402 | lat = Math.PI / 2 - lat; 403 | 404 | //distribute to sphere 405 | return new THREE.Vector3( 406 | Math.sin( lat ) * Math.sin( lng ), 407 | Math.cos( lat ), 408 | Math.sin( lat ) * Math.cos( lng ) 409 | ); 410 | 411 | })( long, lat ); 412 | if (v.x === 0) { v.x = 0.0001; } 413 | const point = this.containerObject.position.clone().add(v); 414 | this.pointCone(cone, point); 415 | 416 | } 417 | 418 | applySoundToCone(cone, sound) { 419 | 420 | sound.scriptNode.onaudioprocess = function() { 421 | let array = new Uint8Array(sound.analyser.frequencyBinCount); 422 | sound.analyser.getByteFrequencyData(array); 423 | let values = 0; 424 | let length = array.length; 425 | for (let i = 0; i < length; i++) values += array[i]; 426 | let average = values / length; 427 | cone.material.opacity = Helpers.mapRange(average, 50, 100, 0.65, 0.95); 428 | } 429 | 430 | sound.spread = cone.sound.spread; 431 | sound.panner.refDistance = cone.sound.panner.refDistance; 432 | sound.panner.distanceModel = cone.sound.panner.distanceModel; 433 | sound.panner.coneInnerAngle = cone.sound.panner.coneInnerAngle; 434 | sound.panner.coneOuterAngle = cone.sound.panner.coneOuterAngle; 435 | sound.panner.coneOuterGain = cone.sound.panner.coneOuterGain; 436 | sound.volume.gain.value = cone.sound.volume.gain.value; 437 | cone.sound = sound; 438 | } 439 | 440 | removeCone(cone) { 441 | cone.sound.source.stop(); 442 | cone.sound.source.disconnect(cone.sound.scriptNode); 443 | cone.sound.scriptNode.disconnect(this.audio.context.destination); 444 | cone.sound = null; 445 | const i = this.cones.indexOf(cone); 446 | this.cones.splice(i, 1); 447 | this.containerObject.remove(cone); 448 | } 449 | 450 | removeFromScene(scene) { 451 | scene.remove(this.containerObject, true); 452 | scene.remove(this.altitudeHelper, true); 453 | scene.remove(this.axisHelper, true); 454 | scene.remove(this.trajectory, true); 455 | 456 | for (const i in this.cones) { 457 | this.cones[i].sound.source.stop(); 458 | } 459 | 460 | if (this.omniSphere.sound && this.omniSphere.sound.source) { 461 | this.omniSphere.sound.source.stop(); 462 | } 463 | } 464 | 465 | pause() { 466 | this.isPaused = true; 467 | } 468 | unpause() { 469 | this.isPaused = false; 470 | } 471 | 472 | mute(main) { 473 | this.isMuted = true; 474 | this.checkMuteState(main); 475 | } 476 | unmute(main) { 477 | this.isMuted = false; 478 | this.checkMuteState(main); 479 | } 480 | 481 | turnVisible() { 482 | this.containerObject.visible = true; 483 | this.axisHelper.visible = true; 484 | this.altitudeHelper.visible = true; 485 | } 486 | 487 | turnInvisible() { 488 | this.containerObject.visible = false; 489 | this.axisHelper.visible = false; 490 | this.altitudeHelper.visible = false; 491 | } 492 | 493 | checkMuteState(main) { 494 | if (main.isMuted || this.isMuted) { 495 | this.cones.forEach(cone => cone.sound.mainMixer.gain.value = 0); 496 | if (this.omniSphere.sound && this.omniSphere.sound.mainMixer) { 497 | this.omniSphere.sound.mainMixer.gain.value = 0; 498 | } 499 | } 500 | else { 501 | this.cones.forEach(cone => cone.sound.mainMixer.gain.value = 1); 502 | if (this.omniSphere.sound && this.omniSphere.sound.mainMixer) { 503 | this.omniSphere.sound.mainMixer.gain.value = 1; 504 | } 505 | } 506 | } 507 | 508 | followTrajectory(mute) { 509 | if (this.trajectory && !this.isPaused && !this.isMuted && !mute) { 510 | this.trajectoryClock -= this.movementDirection * this.movementIncrement; 511 | 512 | if (this.trajectoryClock >= 1) { 513 | if (this.trajectory.spline.closed) { 514 | this.trajectoryClock = 0; 515 | } else { 516 | this.movementDirection = -this.movementDirection; 517 | this.trajectoryClock = 1; 518 | } 519 | } 520 | 521 | if (this.trajectoryClock < 0) { 522 | if (this.trajectory.spline.closed) { 523 | this.trajectoryClock = 1; 524 | } else { 525 | this.movementDirection = -this.movementDirection; 526 | this.trajectoryClock = 0; 527 | } 528 | } 529 | 530 | let pointOnTrajectory = this.trajectory.spline.getPointAt(this.trajectoryClock); 531 | this.containerObject.position.copy(pointOnTrajectory); 532 | this.raycastSphere.position.copy(pointOnTrajectory); 533 | this.altitudeHelper.position.copy(pointOnTrajectory); 534 | this.axisHelper.position.copy(pointOnTrajectory); 535 | this.altitudeHelper.position.y = 0; 536 | 537 | if (this.cones[0]) { 538 | for (const i in this.cones) { 539 | this.setAudioPosition(this.cones[i]); 540 | } 541 | } 542 | if (this.omniSphere.sound) { 543 | this.setAudioPosition(this.omniSphere); 544 | } 545 | } 546 | } 547 | 548 | calculateMovementSpeed() { 549 | if (this.trajectory) { 550 | this.movementIncrement = this.movementSpeed / this.trajectory.spline.getLength(10); 551 | } 552 | } 553 | 554 | toJSON() { 555 | return JSON.stringify({ 556 | filename: (this.omniSphere.sound && this.omniSphere.sound && this.omniSphere.sound.name) || null, 557 | volume: (this.omniSphere && this.omniSphere.sound && this.omniSphere.sound.volume.gain.value) || null, 558 | position: this.containerObject.position, 559 | movementSpeed: this.movementSpeed, 560 | trajectory: (this.trajectory && this.trajectory.points) || null, 561 | cones: this.cones.map((c) => { 562 | return { 563 | file: c.file, 564 | filename: c.filename, 565 | position: { 566 | lat: c.lat, 567 | long: c.long, 568 | }, 569 | volume: c.sound.volume.gain.value, 570 | spread: c.sound.spread, 571 | color: c.randGreen, 572 | }; 573 | }), 574 | }); 575 | } 576 | 577 | fromJSON(json, importedData) { 578 | const object = JSON.parse(json); 579 | this.containerObject.position.copy(object.position); 580 | this.altitudeHelper.position.copy(object.position); 581 | this.raycastSphere.position.copy(object.position); 582 | this.axisHelper.position.copy(object.position); 583 | 584 | if (object.filename && object.volume) { 585 | const file = importedData[object.filename]; 586 | if (file) { 587 | this.loadSound(file, this.audio, false, this).then((sound) => { 588 | this.omniSphere.sound = sound; 589 | this.omniSphere.sound.name = object.filename; 590 | this.omniSphere.sound.volume.gain.value = object.volume; 591 | this.setAudioPosition(this.omniSphere); 592 | }); 593 | } 594 | } 595 | 596 | object.cones.forEach((c) => { 597 | let cone; 598 | const file = importedData[c.filename]; 599 | if (file) { 600 | this.loadSound(file, this.audio, false).then((sound) => { 601 | cone = this.createCone(sound, c.color); 602 | cone.file = file; 603 | cone.filename = c.filename; 604 | cone.sound.volume.gain.value = c.volume; 605 | cone.sound.spread = c.spread; 606 | this.changeLength(cone); 607 | this.changeWidth(cone); 608 | this.gui.addCone(cone); 609 | this.pointConeMagic(cone, c.position.lat, c.position.long); 610 | }); 611 | } 612 | }); 613 | 614 | this.movementSpeed = object.movementSpeed; 615 | } 616 | } 617 | -------------------------------------------------------------------------------- /src/js/app/components/soundtrajectory.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | import Helpers from '../../utils/helpers'; 4 | 5 | export default class SoundTrajectory { 6 | constructor(main, points) { 7 | this.type = 'SoundTrajectory'; 8 | this.splinePoints = points; 9 | this.isActive = true; 10 | this.pointObjects; 11 | this.spline; 12 | this.parentSoundObject; 13 | this.selectedPoint; 14 | this.mouseOffsetX = 0; 15 | this.mouseOffsetY = 0; 16 | this.nonScaledMouseOffsetY = 0; 17 | 18 | this.cursor = new THREE.Mesh( 19 | new THREE.SphereGeometry(15), 20 | new THREE.MeshBasicMaterial({ 21 | color: 0xff1169, 22 | transparent: true, 23 | opacity: 0.5, 24 | }) 25 | ); 26 | this.cursor.visible = false; 27 | main.scene.add(this.cursor); 28 | 29 | /** 30 | * First call to renderPath happens in update trajectory function. From there 31 | * on the function recursively calls itself at the end of each draw. 32 | */ 33 | this.renderPath(); 34 | } 35 | 36 | /** 37 | * A recursive function that draws all the THREE geometries, including 38 | * 39 | * visual and collider spheres as control points on the trajectory spline 40 | * (grouped as points in the pointsObjects array), 41 | * 42 | * the CatmullRom spline that is created from splinePoints, which is populated 43 | * under addPoint function below, and repopulated whenever a trajectory is 44 | * moved or a control point is removed from the spline, 45 | * 46 | * and the line geometry which is assigned as a mesh for the said spline for it 47 | * to be drawn on screen. 48 | */ 49 | renderPath() { 50 | const points = this.splinePoints; 51 | 52 | this.pointObjects = (() => { 53 | const sphere = new THREE.SphereGeometry(10); 54 | const sphereMat = new THREE.MeshBasicMaterial({ color: 0x999999 }); 55 | 56 | const collider = new THREE.SphereGeometry(20); 57 | const colliderMat = new THREE.MeshBasicMaterial({ 58 | color: 0x999999, 59 | transparent: true, 60 | opacity: 0, 61 | depthWrite: false, 62 | }); 63 | const colliderMesh = new THREE.Mesh(collider, colliderMat); 64 | 65 | const pointObjects = []; 66 | 67 | points.forEach((point) => { 68 | const sphereMesh = new THREE.Mesh(sphere, sphereMat.clone()); 69 | const group = new THREE.Object3D(); 70 | 71 | group.add(sphereMesh, colliderMesh.clone()); 72 | group.position.x = point.x; 73 | group.position.y = point.y; 74 | group.position.z = point.z; 75 | 76 | pointObjects.push(group); 77 | }); 78 | 79 | return pointObjects; 80 | })(); 81 | 82 | this.spline = new THREE.CatmullRomCurve3(this.splinePoints); 83 | this.spline.type = 'centripetal'; 84 | 85 | /* 86 | * This measures the distance between the start and end points of a trajectory 87 | * and if the distance is small enough it turnes the spline into a closed curve. 88 | * This is checked in each frame so that the user can interactively determine 89 | * if a trajectory is closed (looping) or open (back-and-forth). 90 | */ 91 | const begEndDistance = this.splinePoints[0].distanceTo(this.splinePoints[this.splinePoints.length - 1]); 92 | 93 | if (begEndDistance < 40) { 94 | this.spline.closed = true; 95 | } else { 96 | this.spline.closed = false; 97 | } 98 | 99 | const geometry = new THREE.Geometry(); 100 | 101 | geometry.vertices = this.spline.getPoints(200); 102 | 103 | const material = new THREE.LineBasicMaterial({ 104 | color: 0x999999, 105 | linewidth: 2, 106 | opacity: 0.4, 107 | }); 108 | 109 | this.spline.mesh = new THREE.Line(geometry, material); 110 | } 111 | 112 | /** 113 | * Creates a global object array that includes both the pointObjects (i.e. vectors 114 | * for both visible and collider spheres) and the spline mesh which is used to 115 | * draw the line that makes up the spline. 116 | */ 117 | get objects() { 118 | return [].concat(this.pointObjects, this.spline.mesh); 119 | } 120 | 121 | /** 122 | * Removes each object in the object array (that pertain to a single 123 | * trajectory) from the scene. 124 | */ 125 | removeFromScene(scene, isUpdating) { 126 | this.objects.forEach((obj) => { 127 | scene.remove(obj, true); 128 | }); 129 | if (!isUpdating) { 130 | scene.remove(this.cursor, true); 131 | } 132 | } 133 | 134 | /** 135 | * Adds each object in the object array (that pertain to a single 136 | * trajectory) to the scene. 137 | */ 138 | addToScene(scene) { 139 | this.objects.forEach((obj) => { 140 | scene.add(obj); 141 | }); 142 | 143 | } 144 | 145 | /** 146 | * Returns true if a particular object is under the mouse (called from 147 | * index.html) 148 | */ 149 | isUnderMouse(raycaster) { 150 | if (this.isActive) { 151 | return raycaster.intersectObjects(this.objects, true).length > 0; 152 | } 153 | } 154 | 155 | /** 156 | * Determines if it is a control point or the curve itself that's under the 157 | * mouse. Returns the collided object. 158 | */ 159 | objectUnderMouse(raycaster) { 160 | const intersects = raycaster.intersectObjects(this.objects, true); 161 | 162 | if (intersects.length > 0) { 163 | if (intersects[0].object.type === 'Line') { 164 | return intersects[Math.floor(intersects.length / 2)]; 165 | } 166 | 167 | return intersects[0]; 168 | } 169 | 170 | return null; 171 | } 172 | 173 | hideCursor() { 174 | this.cursor.visible = false; 175 | } 176 | showCursor(object, point) { 177 | this.cursor.visible = true; 178 | if (object === this.spline.mesh) { 179 | this.cursor.position.copy(point); 180 | } 181 | else { 182 | this.cursor.position.copy(object.parent.position); 183 | } 184 | } 185 | 186 | /* Keeps record of the mouse offset after the initial click. */ 187 | setMouseOffset(nonScaledMouse, point) { 188 | this.mouseOffsetX = point.x; 189 | this.mouseOffsetY = point.z; 190 | this.nonScaledMouseOffsetY = nonScaledMouse.y; 191 | } 192 | 193 | /* Moves a single control point on the spline or the entire trajectory. */ 194 | move(mouse, nonScaledMouse, perspectiveView) { 195 | this.hideCursor(); 196 | if (this.selectedPoint) { 197 | const i = this.pointObjects.indexOf(this.selectedPoint); 198 | 199 | if (i > -1) { 200 | /** 201 | * If the camera is in perspective view, the control points can only be 202 | * moved in the Y-axis (height). 203 | */ 204 | if (perspectiveView) { 205 | let dy = nonScaledMouse.y - this.nonScaledMouseOffsetY; 206 | dy = Helpers.mapRange(dy, -0.5, 0.5, -200, 200); 207 | this.splinePoints[i].y = Math.min(Math.max(this.splinePoints[i].y + dy, -300), 300); 208 | this.nonScaledMouseOffsetY = nonScaledMouse.y; 209 | } else { 210 | /* Otherwise the mouse position vector is copied to the control point. */ 211 | const pointer = mouse.clone(); 212 | pointer.y = this.splinePoints[i].y; 213 | this.splinePoints[i].copy(pointer); 214 | } 215 | 216 | this.updateTrajectory(); 217 | this.selectPoint(this.pointObjects[i]); 218 | } 219 | } else { 220 | /** 221 | * This moves the entire shape when the parent sound object is moved around. 222 | * The same XZ versus Y dimension principles apply depending which view mode 223 | * the camera is in. 224 | */ 225 | if (perspectiveView) { 226 | const posY = Helpers.mapRange( 227 | nonScaledMouse.y - this.nonScaledMouseOffsetY, 228 | -0.5, 229 | 0.5, 230 | -200, 231 | 200, 232 | ); 233 | 234 | this.splinePoints.forEach((pt) => { 235 | pt.y = Math.min(Math.max(-300, pt.y + posY), 300); 236 | }); 237 | 238 | this.nonScaledMouseOffsetY = nonScaledMouse.y; 239 | } else { 240 | /** 241 | * Mouse movement differentials based on initial click position stored 242 | * in setMouseOffset() is calculated here. 243 | */ 244 | const dx = mouse.x - this.mouseOffsetX; 245 | const dy = mouse.z - this.mouseOffsetY; 246 | this.mouseOffsetX = mouse.x; 247 | this.mouseOffsetY = mouse.z; 248 | 249 | /** 250 | * Maps mouse position differentials to the splinePoints, which are in 251 | * return used in the render path function to update the trajectories in 252 | * each frame. 253 | */ 254 | this.splinePoints.forEach((pt) => { 255 | pt.x += dx; 256 | pt.z += dy; 257 | }); 258 | } 259 | 260 | this.updateTrajectory(); 261 | } 262 | } 263 | 264 | setActive() { 265 | this.isActive = true; 266 | 267 | this.pointObjects.forEach((obj) => { 268 | obj.children[0].material.color.setHex(0x999999); 269 | }); 270 | 271 | this.spline.mesh.material.color.setHex(0x999999); 272 | } 273 | 274 | setInactive() { 275 | this.hideCursor(); 276 | this.deselectPoint(); 277 | this.isActive = false; 278 | 279 | this.pointObjects.forEach((obj) => { 280 | obj.children[0].material.color.setHex(0xcccccc); 281 | }); 282 | 283 | this.spline.mesh.material.color.setHex(0xcccccc); 284 | } 285 | 286 | select(intersect, main) { 287 | if (!intersect) return; 288 | 289 | const obj = intersect.object; 290 | 291 | if (obj.type === 'Line') { 292 | this.addPoint(intersect.point); 293 | } else { 294 | this.selectPoint(obj.parent); 295 | this.setMouseOffset(main.nonScaledMouse, intersect.point); 296 | } 297 | } 298 | 299 | selectPoint(obj) { 300 | this.deselectPoint(); 301 | this.selectedPoint = obj; 302 | obj.children[0].material.color.set(0xff0077); 303 | } 304 | 305 | deselectPoint() { 306 | if (this.selectedPoint) { 307 | this.selectedPoint.children[0].material.color.set(0x999999); 308 | this.selectedPoint = null; 309 | } 310 | } 311 | 312 | /** 313 | * Adds new points to an existing trajectory. Updates the splinePoints array 314 | * and calls the updateTrajectory function as a result. 315 | */ 316 | addPoint(position) { 317 | let minDistance = Number.MAX_VALUE; 318 | let minPoint = 1; 319 | let prevDistToSplinePoint = -1; 320 | let closestSplinePoint = 0; 321 | 322 | for (let t = 0; t < 1; t += 1 / 200.0) { 323 | const pt = this.spline.getPoint(t); 324 | 325 | const distToSplinePoint = this.splinePoints[closestSplinePoint].distanceToSquared(pt); 326 | if (distToSplinePoint > prevDistToSplinePoint) { 327 | closestSplinePoint += 1; 328 | 329 | if (closestSplinePoint >= this.splinePoints.length) { 330 | closestSplinePoint = 0; 331 | } 332 | } 333 | 334 | prevDistToSplinePoint = this.splinePoints[closestSplinePoint].distanceToSquared(pt); 335 | 336 | const distToPoint = pt.distanceToSquared(position); 337 | if (distToPoint < minDistance) { 338 | minDistance = distToPoint; 339 | minPoint = closestSplinePoint; 340 | } 341 | } 342 | 343 | this.splinePoints.splice(minPoint, 0, position); 344 | this.updateTrajectory(); 345 | this.selectPoint(this.pointObjects[minPoint]); 346 | } 347 | 348 | /* Removes points from the splinePoints array. */ 349 | removePoint() { 350 | const i = this.pointObjects.indexOf(this.selectedPoint); 351 | this.splinePoints.splice(i, 1); 352 | this.deselectPoint(); 353 | this.updateTrajectory(); 354 | } 355 | 356 | turnVisible() { 357 | this.spline.mesh.visible = true; 358 | this.pointObjects.forEach((point) => { 359 | point.visible = true; 360 | }); 361 | } 362 | 363 | turnInvisible() { 364 | this.spline.mesh.visible = false; 365 | this.pointObjects.forEach((point) => { 366 | point.visible = false; 367 | }); 368 | } 369 | 370 | /** 371 | * Updates trajectory by calling renderPath. First called inside addPoint() 372 | * and this initates the renderPath() recursion. 373 | */ 374 | updateTrajectory() { 375 | const scene = this.spline.mesh.parent; 376 | this.removeFromScene(scene, true); 377 | this.renderPath(); 378 | this.addToScene(scene); 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /src/js/app/components/soundzone.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import 'whatwg-fetch'; 3 | import Helpers from '../../utils/helpers'; 4 | 5 | export default class SoundZone { 6 | constructor(main, points) { 7 | this.type = 'SoundZone'; 8 | this.isActive = true; 9 | this.audio = main.audio; 10 | 11 | this.mouse = main.mouse; 12 | this.scene = main.scene; 13 | 14 | this.points = points; 15 | this.splinePoints = points; 16 | this.pointObjects; 17 | this.spline; 18 | this.shape; 19 | this.sound; 20 | this.loaded = false; 21 | this.isPlaying = false; 22 | this.selectedPoint; 23 | this.mouseOffsetX = 0, this.mouseOffsetY = 0; 24 | this.volume = 1; 25 | 26 | this.containerObject = new THREE.Group(); 27 | this.cursor = new THREE.Mesh( 28 | new THREE.SphereGeometry(15), 29 | new THREE.MeshBasicMaterial({ 30 | color: 0xff1169, 31 | transparent: true, 32 | opacity: 0.5, 33 | }) 34 | ); 35 | this.cursor.visible = false; 36 | this.containerObject.add(this.cursor); 37 | 38 | this.isInitialized = false; 39 | 40 | this.renderPath(); 41 | } 42 | 43 | underUser(audio) { 44 | if (this.sound && !this.isPlaying && this.loaded) { 45 | 46 | this.sound.source = audio.context.createBufferSource(); 47 | this.sound.source.buffer = this.sound.buffer; 48 | this.sound.source.loop = true; 49 | this.sound.source.volume = audio.context.createGain(); 50 | this.sound.source.volume.gain.value = this.volume; 51 | this.sound.source.connect(this.sound.source.volume); 52 | this.sound.source.volume.connect(this.sound.volume); 53 | 54 | this.sound.source.start(audio.context.currentTime); 55 | 56 | this.sound.volume.gain.setTargetAtTime(1.0, audio.context.currentTime + 0.1, 0.1); 57 | this.isPlaying = true; 58 | } 59 | } 60 | 61 | notUnderUser(audio) { 62 | if (this.sound && this.isPlaying && this.loaded) { 63 | this.sound.volume.gain.setTargetAtTime(0.0, audio.context.currentTime, 0.02); 64 | this.sound.source.stop(audio.context.currentTime + 0.1); 65 | this.isPlaying = false; 66 | } 67 | } 68 | 69 | // remove sound file 70 | clear() { 71 | // stop audio stream if currently playing 72 | if (this.isPlaying) { 73 | this.sound.source.stop(); 74 | } 75 | this.isPlaying = false; 76 | this.loaded = false; 77 | this.mainMixer = null; 78 | this.sound = {}; 79 | } 80 | 81 | loadSound(file, audio, mute) { 82 | const context = audio.context; 83 | let reader = new FileReader(); 84 | 85 | this.filename = file.name; 86 | this.file = file; 87 | let that = this; 88 | 89 | reader.onload = (ev) => { 90 | context.decodeAudioData(ev.target.result, function(decodedData) { 91 | that.clear(); 92 | that.sound.name = file.name; 93 | that.sound.source = context.createBufferSource(); 94 | that.sound.source.buffer = decodedData; 95 | that.mainMixer = context.createGain(); 96 | that.sound.volume = context.createGain(); 97 | that.sound.source.volume = context.createGain(); 98 | that.sound.source.connect(that.sound.source.volume); 99 | that.sound.source.volume.connect(that.sound.volume); 100 | that.sound.volume.connect(that.mainMixer); 101 | that.mainMixer.connect(audio.destination); 102 | that.mainMixer.gain.value = mute ? 0 : 1; 103 | that.sound.volume.gain.value = 0.0; 104 | that.sound.buffer = decodedData; 105 | that.loaded = true; 106 | }); 107 | }; 108 | 109 | reader.readAsArrayBuffer(file); 110 | } 111 | 112 | renderPath(args) { 113 | // splinePoints control the curve of the path 114 | const points = this.splinePoints; 115 | 116 | // setup 117 | const sphere = new THREE.SphereGeometry(10); 118 | const sphereMat = new THREE.MeshBasicMaterial({ color: 0xff1169 }); 119 | 120 | const collider = new THREE.SphereGeometry(15); 121 | const colliderMat = new THREE.MeshBasicMaterial({ 122 | color: 0xff1169, 123 | transparent: true, 124 | opacity: 0, 125 | depthWrite: false, 126 | }); 127 | 128 | const colliderMesh = new THREE.Mesh(collider, colliderMat); 129 | 130 | if (!this.isInitialized) { 131 | this.pointObjects = []; 132 | // place a meshgroup at each point in array 133 | points.forEach((point) => { 134 | const sphereMesh = new THREE.Mesh(sphere, sphereMat.clone()); 135 | const group = new THREE.Object3D(); 136 | 137 | group.add(sphereMesh, colliderMesh.clone()); 138 | group.position.copy(point); 139 | 140 | this.pointObjects.push(group); 141 | }); 142 | this.splinePoints = this.pointObjects.map( pt => pt.position ); 143 | } 144 | else if (args) { 145 | if (args.updateType === "delete") { 146 | let splicedPoint = this.pointObjects.splice(args.index, 1); 147 | this.containerObject.remove(splicedPoint[0], true); 148 | } 149 | else if (args.updateType === "add") { 150 | let insertedPoint = new THREE.Object3D(); 151 | insertedPoint.add( 152 | new THREE.Mesh(sphere, sphereMat.clone()), 153 | colliderMesh.clone() 154 | ); 155 | insertedPoint.position.copy(this.splinePoints[args.index]); 156 | this.pointObjects.splice(args.index, 0, insertedPoint); 157 | this.containerObject.add(insertedPoint); 158 | } 159 | this.splinePoints = this.pointObjects.map( pt => pt.position ); 160 | } 161 | 162 | this.spline = new THREE.CatmullRomCurve3(this.splinePoints); 163 | this.spline.type = 'centripetal'; 164 | this.spline.closed = true; 165 | 166 | const geometry = new THREE.Geometry(); 167 | geometry.vertices = this.spline.getPoints(200); 168 | let material = new THREE.LineBasicMaterial({ 169 | color: 0xff1169, 170 | linewidth: 1, 171 | transparent: true, 172 | opacity: 0.4, 173 | }); 174 | 175 | this.spline.mesh = new THREE.Line(geometry, material); 176 | 177 | // fill the path 178 | const rotatedPoints = this.spline.getPoints(200); 179 | rotatedPoints.forEach((vertex) => { 180 | vertex.y = vertex.z; 181 | vertex.z = 0.0; 182 | }); 183 | const shapeFill = new THREE.Shape(); 184 | shapeFill.fromPoints(rotatedPoints); 185 | const shapeGeometry = new THREE.ShapeGeometry(shapeFill); 186 | shapeGeometry.rotateX(Math.PI / 2); 187 | material = new THREE.MeshLambertMaterial({ 188 | color: 0xff1169, 189 | transparent: true, 190 | opacity: Helpers.mapRange(this.volume, 0, 2, 0.05, 0.35), 191 | side: THREE.BackSide, 192 | premultipliedAlpha: true 193 | }); 194 | 195 | this.shape = new THREE.Mesh(shapeGeometry, material); 196 | } 197 | 198 | get objects() { 199 | return [].concat(this.pointObjects, this.spline.mesh, this.shape); 200 | } 201 | 202 | addToScene() { 203 | if (!this.isInitialized) { 204 | this.isInitialized = true; 205 | 206 | var box = new THREE.Box3().setFromObject( this.shape ); 207 | box.getCenter( this.containerObject.position ); 208 | this.scene.add(this.containerObject); 209 | this.objects.forEach((obj) => { 210 | obj.translateX(-this.containerObject.position.x); 211 | obj.translateZ(-this.containerObject.position.z); 212 | this.containerObject.add(obj); 213 | }); 214 | } 215 | else { 216 | this.containerObject.add(this.shape); 217 | this.containerObject.add(this.spline.mesh); 218 | } 219 | } 220 | 221 | removeFromScene(scene) { 222 | this.scene.remove(this.containerObject, true); 223 | } 224 | 225 | // raycast to this soundzone 226 | isUnderMouse(raycaster) { 227 | if (this.isActive) { 228 | return raycaster.intersectObjects(this.objects).length > 0; 229 | } 230 | 231 | return raycaster.intersectObject(this.shape).length > 0; 232 | } 233 | 234 | objectUnderMouse(raycaster) { 235 | const intersects = raycaster.intersectObjects(this.objects, true); 236 | 237 | if (intersects.length > 0) { 238 | if (intersects[0].object.type === 'Line' || intersects[0].object === this.shape) { 239 | return intersects[Math.floor(intersects.length / 2)]; 240 | } 241 | 242 | return intersects[0]; 243 | } 244 | 245 | return null; 246 | } 247 | 248 | hideCursor() { 249 | this.cursor.visible = false; 250 | } 251 | 252 | showCursor(object, point) { 253 | if (object !== this.shape) { 254 | this.cursor.visible = true; 255 | if (object === this.spline.mesh) { 256 | const minv = new THREE.Matrix4().getInverse(this.containerObject.matrix); 257 | this.cursor.position.copy(point.applyMatrix4(minv)); 258 | } 259 | else { 260 | this.cursor.position.copy(object.parent.position); 261 | } 262 | } 263 | else { 264 | this.hideCursor(); 265 | } 266 | } 267 | 268 | setMouseOffset(point) { 269 | this.mouseOffsetX = point.x; 270 | this.mouseOffsetY = point.z; 271 | } 272 | 273 | updateZone(args) { 274 | const scene = this.spline.mesh.parent; 275 | this.containerObject.remove(this.spline.mesh, true); 276 | this.containerObject.remove(this.shape, true); 277 | this.renderPath(args); 278 | this.addToScene(scene); 279 | } 280 | 281 | move(main) { 282 | // if (!main.perspectiveView) { 283 | const dx = main.mouse.x - this.mouseOffsetX; 284 | const dy = main.mouse.z - this.mouseOffsetY; 285 | this.mouseOffsetX = main.mouse.x; 286 | this.mouseOffsetY = main.mouse.z; 287 | this.hideCursor(); 288 | 289 | if (this.selectedPoint) { 290 | // move selected point 291 | const minv = new THREE.Matrix4().getInverse(this.containerObject.matrix); 292 | this.selectedPoint.position.copy(main.mouse.applyMatrix4(minv)); 293 | this.updateZone(); 294 | } else { 295 | // move entire shape 296 | 297 | this.containerObject.position.x += dx; 298 | this.containerObject.position.z += dy; 299 | } 300 | // } 301 | } 302 | 303 | setActive(main) { 304 | this.setMouseOffset(main.mouse); 305 | this.isActive = true; 306 | this.pointObjects.forEach(obj => (obj.visible = true)); 307 | this.spline.mesh.visible = true; 308 | } 309 | 310 | setInactive() { 311 | this.hideCursor(); 312 | this.deselectPoint(); 313 | this.isActive = false; 314 | this.pointObjects.forEach(obj => (obj.visible = false)); 315 | this.spline.mesh.visible = false; 316 | } 317 | 318 | select(intersect) { 319 | if (!intersect) return; 320 | 321 | // obj can be the curve, a spline point, or the shape mesh 322 | const obj = intersect.object; 323 | 324 | if (obj.type === 'Line') { 325 | // add a point to the line 326 | this.addPoint(intersect.point); 327 | } else if (obj.parent.type === 'Object3D') { 328 | // select an existing point on line 329 | this.selectPoint(obj.parent); 330 | } else { 331 | this.deselectPoint(); 332 | this.setMouseOffset(intersect.point); 333 | } 334 | } 335 | 336 | removePoint() { 337 | // find point in array 338 | const i = this.pointObjects.indexOf(this.selectedPoint); 339 | this.splinePoints.splice(i, 1); 340 | this.deselectPoint(); 341 | this.updateZone({index: i, updateType: 'delete'}); 342 | } 343 | 344 | addPoint(point) { 345 | const minv = new THREE.Matrix4().getInverse(this.containerObject.matrix); 346 | const position = point.applyMatrix4(minv); 347 | 348 | let closestSplinePoint = 0; 349 | let prevDistToSplinePoint = -1; 350 | let minDistance = Number.MAX_VALUE; 351 | let minPoint = 1; 352 | 353 | // search for point on spline 354 | for (let t = 0; t < 1; t += 1 / 200.0) { 355 | const pt = this.spline.getPoint(t); 356 | 357 | const distToSplinePoint = this.splinePoints[closestSplinePoint].distanceToSquared(pt); 358 | if (distToSplinePoint > prevDistToSplinePoint) { 359 | closestSplinePoint += 1; 360 | 361 | if (closestSplinePoint >= this.splinePoints.length) { 362 | closestSplinePoint = 0; 363 | } 364 | } 365 | prevDistToSplinePoint = this.splinePoints[closestSplinePoint].distanceToSquared(pt); 366 | const distToPoint = pt.distanceToSquared(position); 367 | if (distToPoint < minDistance) { 368 | minDistance = distToPoint; 369 | minPoint = closestSplinePoint; 370 | } 371 | } 372 | 373 | this.splinePoints.splice(minPoint, 0, position); 374 | this.updateZone({index: minPoint, updateType: 'add'}); 375 | this.selectPoint(this.pointObjects[minPoint]); 376 | } 377 | 378 | selectPoint(obj) { 379 | this.deselectPoint(); 380 | this.selectedPoint = obj; 381 | obj.children[0].material.color.set('blue'); 382 | } 383 | 384 | deselectPoint() { 385 | if (this.selectedPoint) { 386 | this.selectedPoint.children[0].material.color.set('red'); 387 | this.selectedPoint = null; 388 | } 389 | } 390 | 391 | mute(main) { 392 | this.isMuted = true; 393 | this.checkMuteState(main); 394 | } 395 | 396 | unmute(main) { 397 | this.isMuted = false; 398 | this.checkMuteState(main); 399 | } 400 | 401 | turnVisible() { 402 | 403 | } 404 | 405 | turnInvisible() { 406 | this.shape.material.visible = false; 407 | this.pointObjects.forEach((point) => { 408 | point.children[0].visible = false; 409 | }); 410 | this.spline.mesh.material.visible = false; 411 | } 412 | 413 | checkMuteState(main) { 414 | if (this.mainMixer) { 415 | if (main.isMuted || this.isMuted) { 416 | this.mainMixer.gain.value = 0; 417 | } 418 | else { 419 | this.mainMixer.gain.value = 1; 420 | } 421 | } 422 | } 423 | 424 | toJSON() { 425 | const object = { 426 | position: this.containerObject.position, 427 | points: this.splinePoints, 428 | filename: this.filename, 429 | volume: this.volume 430 | }; 431 | return JSON.stringify(object); 432 | } 433 | 434 | fromJSON(json, importedData) { 435 | const object = JSON.parse(json); 436 | this.containerObject.position.copy(object.position); 437 | let file = importedData[object.filename]; 438 | 439 | if (file) { 440 | this.loadSound(file, this.audio, false); 441 | const volume = Math.max(Math.min(object.volume, 2), 0.0); 442 | this.shape.material.opacity = Helpers.mapRange(volume, 0, 2, 0.05, 0.35); 443 | this.volume = volume; 444 | if (this.sound && this.sound.source) { 445 | this.sound.source.volume.gain.value = volume; 446 | } 447 | } 448 | } 449 | } 450 | -------------------------------------------------------------------------------- /src/js/app/helpers/animation.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | export default class Animation { 4 | constructor(obj, clip) { 5 | // Object with animations 6 | this.obj = obj; 7 | 8 | // Initialize animation mixer 9 | this.mixer = new THREE.AnimationMixer(this.obj); 10 | 11 | // Simple animation player 12 | this.playClip(clip); 13 | } 14 | 15 | playClip(clip) { 16 | this.action = this.mixer.clipAction(clip); 17 | 18 | this.action.play(); 19 | } 20 | 21 | // Call update in loop 22 | update(delta) { 23 | if(this.mixer) { 24 | this.mixer.update(delta); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/js/app/helpers/geometry.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | import Material from './material'; 4 | 5 | import Config from '../../data/config'; 6 | 7 | // This helper class can be used to create and then place geometry in the scene 8 | export default class Geometry { 9 | constructor(scene) { 10 | this.scene = scene; 11 | this.geo = null; 12 | } 13 | 14 | make(type) { 15 | if(type == 'plane') { 16 | return (width, height, widthSegments = 1, heightSegments = 1) => { 17 | this.geo = new THREE.PlaneGeometry(width, height, widthSegments, heightSegments); 18 | }; 19 | } 20 | 21 | if(type == 'sphere') { 22 | return (radius, widthSegments = 32, heightSegments = 32) => { 23 | this.geo = new THREE.SphereGeometry(radius, widthSegments, heightSegments); 24 | }; 25 | } 26 | } 27 | 28 | place(position, rotation) { 29 | const material = new Material(0xffffff).standard; 30 | const mesh = new THREE.Mesh(this.geo, material); 31 | 32 | // Use ES6 spread to set position and rotation from passed in array 33 | mesh.position.set(...position); 34 | mesh.rotation.set(...rotation); 35 | 36 | if(Config.shadow.enabled) { 37 | mesh.receiveShadow = true; 38 | } 39 | 40 | this.scene.add(mesh); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/js/app/helpers/material.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | import Config from '../../data/config'; 4 | 5 | // Use this class as a helper to set up some default materials 6 | export default class Material { 7 | constructor(color) { 8 | this.basic = new THREE.MeshBasicMaterial({ 9 | color, 10 | side: THREE.DoubleSide, 11 | }); 12 | 13 | this.standard = new THREE.MeshStandardMaterial({ 14 | color, 15 | side: THREE.DoubleSide, 16 | visible: false, 17 | }); 18 | 19 | this.wire = new THREE.MeshBasicMaterial({ wireframe: false }); 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /src/js/app/helpers/meshHelper.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | // Simple mesh helper that shows edges, wireframes, and face and vertex normals 4 | export default class MeshHelper { 5 | constructor(scene, mesh) { 6 | const wireframe = new THREE.WireframeGeometry(mesh.geometry); 7 | const wireLine = new THREE.LineSegments(wireframe); 8 | wireLine.material.depthTest = false; 9 | wireLine.material.opacity = 0.25; 10 | wireLine.material.transparent = true; 11 | mesh.add(wireLine); 12 | 13 | const edges = new THREE.EdgesGeometry(mesh.geometry); 14 | const edgesLine = new THREE.LineSegments(edges); 15 | edgesLine.material.depthTest = false; 16 | edgesLine.material.opacity = 0.25; 17 | edgesLine.material.transparent = true; 18 | mesh.add(edgesLine); 19 | 20 | scene.add(new THREE.BoxHelper(mesh)); 21 | scene.add(new THREE.FaceNormalsHelper(mesh, 2)); 22 | scene.add(new THREE.VertexNormalsHelper(mesh, 2)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/js/app/managers/datGUI.js: -------------------------------------------------------------------------------- 1 | import Config from '../../data/config'; 2 | 3 | // Manages all dat.GUI interactions 4 | export default class DatGUI { 5 | constructor(main) { 6 | const gui = new dat.GUI({ autoPlace: false }); 7 | 8 | const guiContainer = document.getElementById('guis'); 9 | guiContainer.appendChild(gui.domElement); 10 | 11 | gui.width = 250; 12 | gui.add(main, 'loadFile').name('Load Sound File'); 13 | gui.add(main, 'attach').name('Attach Sound'); 14 | gui.add(main, 'toggleAddTrajectory').name('Add Trajectory'); 15 | gui.add(main, 'editObject').name('Edit Object'); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/js/app/managers/interaction.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import Keyboard from '../../utils/keyboard'; 3 | import Config from '../../data/config'; 4 | 5 | // Manages all input interactions 6 | export default class Interaction { 7 | constructor(main, renderer, scene, camera, controls) { 8 | // Properties 9 | this.renderer = renderer; 10 | this.scene = scene; 11 | this.camera = camera; 12 | this.controls = controls; 13 | 14 | this.timeout = null; 15 | 16 | // Instantiate keyboard helper 17 | this.keyboard = new Keyboard(); 18 | 19 | // Listeners 20 | // Mouse events 21 | this.renderer.domElement.addEventListener('mousemove', event => this.onMouseMove(main, event), false); 22 | this.renderer.domElement.addEventListener('mouseleave', event => this.onMouseUp(main, event, false), false); 23 | this.renderer.domElement.addEventListener('mouseover', event => this.onMouseOver(event), false); 24 | this.renderer.domElement.addEventListener('mouseup', event => this.onMouseUp(main, event, true), false); 25 | this.renderer.domElement.addEventListener('mousedown', event => this.onMouseDown(main, event), false); 26 | 27 | // Keyboard events 28 | this.keyboard.domElement.addEventListener('keydown', (event) => { 29 | // Only once 30 | if (event.repeat) { 31 | return; 32 | } 33 | 34 | if (this.keyboard.eventMatches(event, 'w')) { 35 | document.getElementById('help-head').style.display = 'none'; 36 | main.moveForward = 1 * main.movementSpeed; 37 | } 38 | 39 | if (this.keyboard.eventMatches(event, 'a')) { 40 | main.yawRight = 1 * main.rotationSpeed; 41 | } 42 | 43 | if (this.keyboard.eventMatches(event, 'd')) { 44 | main.yawLeft = 1 * main.rotationSpeed; 45 | } 46 | 47 | if (this.keyboard.eventMatches(event, 's')) { 48 | main.moveBackwards = 1 * main.movementSpeed; 49 | } 50 | 51 | // if (this.keyboard.eventMatches(event, 'e')) { 52 | // this.data = main.export(); 53 | // 54 | // const a = document.createElement('a'); 55 | // const blob = new Blob([this.data], {'type':'text/plain'}); 56 | // a.href = window.URL.createObjectURL(blob); 57 | // a.download = 'export.json'; 58 | // a.click(); 59 | // } 60 | 61 | // if (this.keyboard.eventMatches(event, 'i')) { 62 | // const i = document.getElementById('import'); 63 | // i.click(); 64 | // i.addEventListener('change', handleFiles, false); 65 | // 66 | // function handleFiles() { 67 | // const reader = new FileReader(); 68 | // reader.addEventListener('load', (e) => { 69 | // main.import(e.target.result); 70 | // }); 71 | // reader.readAsText(this.files[0]); 72 | // } 73 | // } 74 | 75 | if (this.keyboard.eventMatches(event, 'backspace') || 76 | this.keyboard.eventMatches(event, 'delete')) { 77 | 78 | if (main.activeObject && main.activeObject.type === 'SoundTrajectory') { 79 | if (main.activeObject.selectedPoint && main.activeObject.splinePoints.length > 2) { 80 | main.activeObject.removePoint(); 81 | } 82 | } 83 | 84 | if (main.activeObject && main.activeObject.type === 'SoundZone') { 85 | if (main.activeObject.selectedPoint && main.activeObject.splinePoints.length > 3) { 86 | main.activeObject.removePoint(); 87 | } else { 88 | main.removeSoundZone(main.activeObject); 89 | main.activeObject = null; 90 | } 91 | } 92 | 93 | if (main.activeObject && main.activeObject.type === 'SoundObject') { 94 | if(main.isEditingObject){ 95 | if (main.interactiveCone) { 96 | main.removeCone(main.activeObject, main.interactiveCone); 97 | } 98 | }else{ 99 | main.removeSoundObject(main.activeObject); 100 | 101 | if (main.activeObject.trajectory) 102 | main.removeSoundTrajectory(main.activeObject.trajectory); 103 | 104 | main.activeObject = null; 105 | } 106 | } 107 | } 108 | 109 | if(Config.isDev) { 110 | if (this.keyboard.eventMatches(event, 'h')) { 111 | const base = document.getElementsByClassName('rs-base')[0]; 112 | 113 | if (base.style.display === 'none') base.style.display = 'block'; 114 | else base.style.display = 'none'; 115 | } 116 | } 117 | }); 118 | 119 | this.keyboard.domElement.addEventListener('keyup', (event) => { 120 | // Only once 121 | if (event.repeat) { 122 | return; 123 | } 124 | 125 | if (this.keyboard.eventMatches(event, 'w')) { 126 | main.moveForward = 0; 127 | } 128 | 129 | if (this.keyboard.eventMatches(event, 'a')) { 130 | main.yawRight = 0; 131 | } 132 | 133 | if (this.keyboard.eventMatches(event, 'd')) { 134 | main.yawLeft = 0; 135 | } 136 | 137 | if (this.keyboard.eventMatches(event, 's')) { 138 | main.moveBackwards = 0; 139 | } 140 | 141 | // if (this.keyboard.eventMatches(event, 'r')) { 142 | // main.reset(true); 143 | // } 144 | 145 | if (this.keyboard.eventMatches(event, 'u')) { 146 | main.isUserStudyLoading = !main.isUserStudyLoading; 147 | document.getElementById('help-add').style.display = 'none'; 148 | document.getElementById('help-camera').style.display = 'none'; 149 | document.getElementById('help-head').style.display = 'none'; 150 | } 151 | }); 152 | } 153 | 154 | onMouseOver(event) { 155 | event.preventDefault(); 156 | 157 | Config.isMouseOver = true; 158 | } 159 | 160 | onMouseLeave(event) { 161 | event.preventDefault(); 162 | 163 | Config.isMouseOver = false; 164 | } 165 | 166 | onMouseMove(main, event) { 167 | event.preventDefault(); 168 | 169 | clearTimeout(this.timeout); 170 | this.timeout = setTimeout(() => { Config.isMouseMoving = false; }, 200); 171 | 172 | Config.isMouseMoving = true; 173 | 174 | main.setMousePosition(event); 175 | if (main.isMouseDown === true && !main.isEditingObject) { 176 | if (main.isAddingTrajectory === true) { 177 | if (main.activeObject.type === 'SoundObject') { 178 | main.mouse.y = main.activeObject.containerObject.position.y; 179 | main.path.addPoint(main.mouse); 180 | } 181 | } 182 | 183 | if (main.isAddingObject === true) { 184 | main.path.addPoint(main.mouse); 185 | } 186 | 187 | if (main.activeObject) { 188 | if (main.activeObject.type === 'SoundTrajectory') { 189 | main.activeObject.move(main.mouse, main.nonScaledMouse, main.perspectiveView); 190 | } 191 | else { 192 | main.activeObject.move(main); 193 | } 194 | } 195 | }//end if(main.isMouseDown...) 196 | 197 | // show cursor on hover 198 | else if (!main.isEditingObject && main.activeObject) { 199 | // make sure object to raycast to is the trajectory 200 | let obj = main.activeObject; 201 | if (obj.type === 'SoundObject') { 202 | if (obj.trajectory) { 203 | obj = obj.trajectory; 204 | } 205 | else { 206 | return; 207 | } 208 | } 209 | 210 | switch (obj.type) { 211 | case 'SoundTrajectory': 212 | case 'SoundZone': 213 | const intersection = obj.objectUnderMouse(main.ray); 214 | if (intersection) { 215 | obj.showCursor(intersection.object, intersection.point); 216 | } 217 | else { 218 | obj.hideCursor(); 219 | } 220 | break; 221 | default: 222 | break; 223 | } 224 | 225 | } 226 | 227 | if (main.isEditingObject) { 228 | let intersect3; 229 | 230 | if (main.isMouseDown) { 231 | // point cone towards mouse pointer 232 | intersect3 = main.ray.intersectObject(main.activeObject.raycastSphere)[0]; 233 | 234 | if (main.interactiveCone != null && intersect3) { 235 | main.activeObject.pointCone(main.interactiveCone, intersect3.point); 236 | } 237 | else { 238 | // console.log('no cone is a snow cone') 239 | } 240 | } 241 | else { 242 | intersect3 = main.ray.intersectObjects(main.activeObject.cones)[0]; 243 | 244 | // temp set color on hover 245 | main.activeObject.cones.forEach(cone => { 246 | if (intersect3 && intersect3.object.uuid === cone.uuid) { 247 | cone.isHighlighted = true; 248 | cone.material.color.set(cone.hoverColor()); 249 | } 250 | else if (cone.isHighlighted) { 251 | cone.isHighlighted = false; 252 | cone.material.color.set(cone.baseColor); 253 | } 254 | }); 255 | } 256 | }//end if(main.isEditingObject) 257 | } 258 | 259 | onMouseUp(main, event, hasFocus) { 260 | // turn gui pointer events back on 261 | main.gui.enable(); 262 | document.getElementById('GlobalContainer').style.pointerEvents = 'auto'; 263 | 264 | // turn controls back on 265 | main.controls.enable(); 266 | 267 | // mouse leaves the container 268 | if (!hasFocus) { Config.isMouseOver = false; } 269 | if (main.isMouseDown === false) { return; } 270 | 271 | // actual mouseup interaction 272 | main.setMousePosition(event); 273 | let obj; 274 | 275 | if (main.isAddingTrajectory) { 276 | obj = main.path.createObject(main); 277 | 278 | main.toggleAddTrajectory(false); 279 | } 280 | 281 | if (main.isAddingObject) { 282 | obj = main.path.createObject(main); 283 | 284 | main.setActiveObject(obj); 285 | main.toggleAddObject(); 286 | main.isAddingObject = false; 287 | } 288 | 289 | if (main.isEditingObject) { 290 | if (main.interactiveCone) { 291 | main.interactiveCone.material.color.set(main.interactiveCone.baseColor); 292 | } 293 | } 294 | 295 | main.isMouseDown = false; 296 | 297 | for (const i in main.soundObjects) { 298 | if (main.soundObjects[i].type === 'SoundObject') main.soundObjects[i].calculateMovementSpeed(); 299 | } 300 | } 301 | 302 | onMouseDown(main, event) { 303 | // turn gui events off when interacting with scene objects 304 | main.gui.disable(); 305 | document.getElementById('GlobalContainer').style.pointerEvents = 'none'; 306 | 307 | /** 308 | * !keyPressed is added to avoid interaction with object when the camera 309 | * is being rotated. It can (should) be changed into a flag more specific 310 | * to this action. 311 | */ 312 | if (!main.keyPressed) { 313 | main.isMouseDown = true; 314 | 315 | /** 316 | * Create a collection array of all the object in the scene and check if 317 | * any of these objects isUnderMouse (a function which is passed our raycaster). 318 | * 319 | * If there is indeed an intersected object, set it as the activeObject. 320 | */ 321 | const everyComponent = [].concat(main.soundObjects, main.soundTrajectories, main.soundZones); 322 | const intersectObjects = everyComponent.filter((obj) => { 323 | return obj.isUnderMouse(main.ray); 324 | }); 325 | 326 | // if adding trajectory, check that mousedown is valid 327 | main.isAddingTrajectory = (main.isAddingTrajectory && intersectObjects[0] === main.activeObject); 328 | 329 | // set activeObject to intersected object 330 | if (!main.isEditingObject) { 331 | if (intersectObjects.length > 0 && !main.isAddingObject) { 332 | // if soundzones overlap, keep last selected 333 | if (!(main.activeObject && main.activeObject.type === 'SoundZone' && intersectObjects[0].type === 'SoundZone' && intersectObjects.indexOf(main.activeObject) > -1)) { 334 | main.setActiveObject(intersectObjects[0]); 335 | } 336 | } 337 | else { 338 | main.setActiveObject(null); 339 | } 340 | } 341 | 342 | // disable controls when add or moving an object 343 | if (main.isAddingTrajectory || main.isAddingObject || (main.activeObject && !main.isEditingObject)) { 344 | main.controls.disable(); 345 | } 346 | 347 | /** 348 | * If adding a trajectory, ask the trajectory interface to initate a new 349 | * trajectory at the mouse position determined in setMousePosition() 350 | * 351 | * Same for the zone. 352 | */ 353 | if (main.isAddingTrajectory) { 354 | main.mouse.y = main.activeObject.containerObject.position.y; 355 | main.path.beginAt(main.mouse, main.activeObject); 356 | } 357 | 358 | if (main.isAddingObject) { 359 | main.path.beginAt(main.mouse); 360 | } 361 | 362 | /* If the most recent active object interacted with again, select it: */ 363 | if (main.activeObject && main.activeObject.isUnderMouse(main.ray)) { 364 | // click inside active object 365 | if (main.activeObject.type != 'SoundObject'){ 366 | const intersect = main.activeObject.objectUnderMouse(main.ray); 367 | main.activeObject.select(intersect, main); 368 | } else { 369 | main.activeObject.select(main); 370 | } 371 | } 372 | 373 | /** 374 | * In object edit mode a different interaction scheme is followed. 375 | */ 376 | if (main.isEditingObject) { 377 | if (!main.activeObject || !main.activeObject.cones) { 378 | console.log('wheres my cone :(', main.activeObject); //error check 379 | } 380 | 381 | const intersect2 = main.ray.intersectObjects(main.activeObject.cones)[0]; 382 | if (intersect2) { 383 | main.interactiveCone = intersect2.object; 384 | main.controls.disable(); 385 | } 386 | else { 387 | main.interactiveCone = null; 388 | } 389 | } 390 | } 391 | } 392 | } 393 | -------------------------------------------------------------------------------- /src/js/app/model/model.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | import Material from '../helpers/material'; 4 | import MeshHelper from '../helpers/meshHelper'; 5 | import Helpers from '../../utils/helpers'; 6 | import Config from '../../data/config'; 7 | 8 | // Loads in a single object from the config file 9 | export default class Model { 10 | constructor(scene, loader) { 11 | this.scene = scene; 12 | 13 | // Manager is passed in to loader to determine when loading done in main 14 | this.loader = loader; 15 | this.obj = null; 16 | } 17 | 18 | load() { 19 | // Load model with ObjectLoader 20 | this.loader.load(Config.model.path, obj => { 21 | obj.traverse((child) => { 22 | if (child instanceof THREE.Mesh) { 23 | // Create material for mesh and set its map to texture by name from preloaded textures 24 | const material = new Material(0x44aaff).basic; 25 | child.material = material; 26 | material.transparent = true; 27 | material.opacity = 0.8; 28 | 29 | // Set to cast and receive shadow if enabled 30 | if (Config.shadow.enabled) { 31 | child.receiveShadow = true; 32 | child.castShadow = true; 33 | } 34 | } 35 | }); 36 | 37 | // Set prop to obj 38 | this.obj = obj; 39 | 40 | obj.name = 'dummyHead'; 41 | 42 | obj.position.y = 1; // necessary for raycasting onto the zone shape 43 | obj.rotation.y += Math.PI; 44 | obj.scale.multiplyScalar(Config.model.scale); 45 | 46 | this.scene.add(obj); 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/js/app/model/texture.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | // Promise polyfill for IE 3 | import { Promise } from 'es6-promise'; 4 | 5 | import Helpers from '../../utils/helpers'; 6 | import Config from '../../data/config'; 7 | 8 | // This class preloads all textures in the imageFiles array in the Config via ES6 Promises. 9 | // Once all textures are done loading the model itself will be loaded after the Promise .then() callback. 10 | // Using promises to preload textures prevents issues when applying textures to materials 11 | // before the textures have loaded. 12 | export default class Texture { 13 | constructor() { 14 | // Prop that will contain all loaded textures 15 | this.textures = {}; 16 | } 17 | 18 | load() { 19 | const loader = new THREE.TextureLoader(); 20 | const maxAnisotropy = Config.maxAnisotropy; 21 | const imageFiles = Config.texture.imageFiles; 22 | const promiseArray = []; 23 | 24 | loader.setPath(Config.texture.path); 25 | 26 | imageFiles.forEach(imageFile => { 27 | // Add an individual Promise for each image in array 28 | promiseArray.push(new Promise((resolve, reject) => { 29 | // Each Promise will attempt to load the image file 30 | loader.load(imageFile.image, 31 | // This gets called on load with the loaded texture 32 | texture => { 33 | texture.anisotropy = maxAnisotropy; 34 | 35 | // Resolve Promise with object of texture if it is instance of THREE.Texture 36 | const modelOBJ = {}; 37 | modelOBJ[imageFile.name] = texture; 38 | if(modelOBJ[imageFile.name] instanceof THREE.Texture) 39 | resolve(modelOBJ); 40 | }, 41 | Helpers.logProgress(), 42 | xhr => reject(new Error(xhr + 'An error occurred loading while loading ' + imageFile.image)) 43 | ) 44 | })); 45 | }); 46 | 47 | // Iterate through all Promises in array and return another Promise when all have resolved or console log reason when any reject 48 | return Promise.all(promiseArray).then(textures => { 49 | // Set the textures prop object to have name be the resolved texture 50 | for(let i = 0; i < textures.length; i++) { 51 | this.textures[Object.keys(textures[i])[0]] = textures[i][Object.keys(textures[i])[0]]; 52 | } 53 | }, reason => console.log(reason)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/js/data/config.js: -------------------------------------------------------------------------------- 1 | import TWEEN from 'tween.js'; 2 | 3 | // This object contains the state of the app 4 | export default { 5 | isDev: false, 6 | isLoaded: false, 7 | isTweening: false, 8 | isRotating: true, 9 | isMouseMoving: false, 10 | isMouseOver: false, 11 | maxAnisotropy: 1, 12 | dpr: 1, 13 | easing: TWEEN.Easing.Quadratic.InOut, 14 | duration: 500, 15 | model: { 16 | path: './assets/models/head.obj', 17 | scale: 30 18 | }, 19 | texture: { 20 | path: './assets/textures/', 21 | imageFiles: [] 22 | }, 23 | mesh: { 24 | enableHelper: false, 25 | wireframe: true, 26 | translucent: false, 27 | material: { 28 | color: 0xffffff, 29 | emissive: 0xffffff 30 | } 31 | }, 32 | fog: { 33 | color: 0xffffff, 34 | near: 0.0008 35 | }, 36 | grid: { 37 | size: 10000, 38 | divisions: 80 39 | }, 40 | camera: { 41 | fov: 45, 42 | near: 100, 43 | far: 10000, 44 | aspect: 1, 45 | posX: 0, 46 | posY: 2500, 47 | posZ: 0 48 | }, 49 | soundObject: { 50 | defaultRadius: 50, 51 | defaultTrajectoryClock: 1, 52 | defaultMovementSpeed: 5, 53 | defaultMovementDirection: 1, 54 | defaultPosX: 0, 55 | defaultPosY: 0, 56 | defaultPosZ: 0 57 | }, 58 | controls: { 59 | autoRotate: false, 60 | autoRotateSpeed: -0.5, 61 | rotateSpeed: 0.5, 62 | zoomSpeed: 0.8, 63 | minDistance: 200, 64 | maxDistance: 5000, 65 | minPolarAngle: -Math.PI / 2, 66 | maxPolarAngle: Math.PI / 2, 67 | minAzimuthAngle: -Infinity, 68 | maxAzimuthAngle: Infinity, 69 | enableDamping: true, 70 | dampingFactor: 0.5, 71 | enableZoom: true, 72 | target: { 73 | x: 0, 74 | y: 0, 75 | z: 0 76 | } 77 | }, 78 | ambientLight: { 79 | enabled: true, 80 | color: 0x777777 81 | }, 82 | directionalLight: { 83 | enabled: true, 84 | color: 0xffffff, 85 | intensity: 1, 86 | multiplyScalar: 50, 87 | hue: 0.1, 88 | saturation: 0, 89 | lightness: 0.5, 90 | x: 0, 91 | y: 30, 92 | z: 0 93 | }, 94 | shadow: { 95 | enabled: true, 96 | helperEnabled: false, 97 | bias: 0, 98 | mapWidth: 2048, 99 | mapHeight: 2048, 100 | near: 0, 101 | far: 2048, 102 | top: 2048, 103 | right: 2048, 104 | bottom: -2048, 105 | left: -2048 106 | }, 107 | pointLight: { 108 | enabled: false, 109 | color: 0xffffff, 110 | intensity: 0.34, 111 | distance: 115, 112 | x: 0, 113 | y: 0, 114 | z: 0 115 | }, 116 | hemiLight: { 117 | enabled: false, 118 | color: 0xc8c8c8, 119 | groundColor: 0xffffff, 120 | intensity: 0.55, 121 | x: 0, 122 | y: 0, 123 | z: 0 124 | } 125 | }; 126 | -------------------------------------------------------------------------------- /src/js/utils/detector.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author alteredq / http://alteredqualia.com/ 3 | * @author mr.doob / http://mrdoob.com/ 4 | */ 5 | 6 | export default { 7 | canvas: !!window.CanvasRenderingContext2D, 8 | webgl: (function() { 9 | try { 10 | var canvas = document.createElement('canvas'); 11 | 12 | return !!(window.WebGLRenderingContext && (canvas.getContext('webgl') || canvas.getContext('experimental-webgl'))); 13 | } catch(e) { 14 | return false; 15 | } 16 | })(), 17 | 18 | workers: !!window.Worker, 19 | fileapi: window.File && window.FileReader && window.FileList && window.Blob, 20 | 21 | getWebGLErrorMessage: function() { 22 | var element = document.createElement('div'); 23 | element.id = 'webgl-error-message'; 24 | element.style.fontFamily = 'monospace'; 25 | element.style.fontSize = '13px'; 26 | element.style.fontWeight = 'normal'; 27 | element.style.textAlign = 'center'; 28 | element.style.background = '#fff'; 29 | element.style.color = '#000'; 30 | element.style.padding = '1.5em'; 31 | element.style.width = '400px'; 32 | element.style.margin = '5em auto 0'; 33 | 34 | if(!this.webgl) { 35 | element.innerHTML = window.WebGLRenderingContext ? [ 36 | 'Your graphics card does not seem to support WebGL.
', 37 | 'Find out how to get it here.' 38 | ].join('\n') : [ 39 | 'Your browser does not seem to support WebGL.
', 40 | 'Find out how to get it here.' 41 | ].join('\n'); 42 | } 43 | 44 | return element; 45 | }, 46 | 47 | addGetWebGLMessage: function(parameters) { 48 | var parent, id, element; 49 | 50 | parameters = parameters || {}; 51 | 52 | parent = parameters.parent !== undefined ? parameters.parent : document.body; 53 | id = parameters.id !== undefined ? parameters.id : 'oldie'; 54 | 55 | element = this.getWebGLErrorMessage(); 56 | element.id = id; 57 | 58 | parent.appendChild(element); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /src/js/utils/helpers.js: -------------------------------------------------------------------------------- 1 | // Provides simple static functions that are used multiple times in the app 2 | export default class Helpers { 3 | static throttle(fn, threshhold, scope) { 4 | threshhold || (threshhold = 250); 5 | let last, deferTimer; 6 | 7 | return function() { 8 | const context = scope || this; 9 | 10 | const now = +new Date, 11 | args = arguments; 12 | 13 | if(last && now < last + threshhold) { 14 | clearTimeout(deferTimer); 15 | deferTimer = setTimeout(function() { 16 | last = now; 17 | fn.apply(context, args); 18 | }, threshhold); 19 | } 20 | else { 21 | last = now; 22 | fn.apply(context, args); 23 | } 24 | }; 25 | } 26 | 27 | static logProgress() { 28 | return function(xhr) { 29 | if(xhr.lengthComputable) { 30 | const percentComplete = xhr.loaded / xhr.total * 100; 31 | 32 | console.log(Math.round(percentComplete, 2) + '% downloaded'); 33 | } 34 | } 35 | } 36 | 37 | static logError() { 38 | return function(xhr) { 39 | console.error(xhr); 40 | } 41 | } 42 | 43 | static handleColorChange(color) { 44 | return (value) => { 45 | if(typeof value === 'string') { 46 | value = value.replace('#', '0x'); 47 | } 48 | 49 | color.setHex(value); 50 | }; 51 | } 52 | 53 | static update(mesh) { 54 | this.needsUpdate(mesh.material, mesh.geometry); 55 | } 56 | 57 | static needsUpdate(material, geometry) { 58 | return function() { 59 | material.shading = +material.shading; //Ensure number 60 | material.vertexColors = +material.vertexColors; //Ensure number 61 | material.side = +material.side; //Ensure number 62 | material.needsUpdate = true; 63 | geometry.verticesNeedUpdate = true; 64 | geometry.normalsNeedUpdate = true; 65 | geometry.colorsNeedUpdate = true; 66 | }; 67 | } 68 | 69 | static updateTexture(material, materialKey, textures) { 70 | return function(key) { 71 | material[materialKey] = textures[key]; 72 | material.needsUpdate = true; 73 | }; 74 | } 75 | 76 | static toRadians(angle) { 77 | return angle * (Math.PI / 180); 78 | } 79 | 80 | static mapRange(value, low1, high1, low2, high2) { 81 | return low2 + (high2 - low2) * (value - low1) / (high1 - low1); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/js/utils/keyboard.js: -------------------------------------------------------------------------------- 1 | const ALIAS = { 2 | 'left' : 37, 3 | 'up' : 38, 4 | 'right' : 39, 5 | 'down' : 40, 6 | 'space' : 32, 7 | 'tab' : 9, 8 | 'backspace' : 8, 9 | 'delete' : 46, 10 | 'escape' : 27 11 | }; 12 | 13 | export default class Keyboard { 14 | constructor(domElement) { 15 | this.domElement = domElement || document; 16 | this.keyCodes = {}; 17 | 18 | // bind keyEvents 19 | this.domElement.addEventListener('keydown', (event) => this.onKeyChange(event), false); 20 | this.domElement.addEventListener('keyup', (event) => this.onKeyChange(event), false); 21 | 22 | // bind window blur 23 | window.addEventListener('blur', () => this.onBlur, false); 24 | } 25 | 26 | destroy() { 27 | this.domElement.removeEventListener('keydown', (event) => this.onKeyChange(event), false); 28 | this.domElement.removeEventListener('keyup', (event) => this.onKeyChange(event), false); 29 | 30 | // unbind window blur event 31 | window.removeEventListener('blur', () => this.onBlur, false); 32 | } 33 | 34 | onBlur() { 35 | for(const prop in this.keyCodes) 36 | this.keyCodes[prop] = false; 37 | } 38 | 39 | onKeyChange(event) { 40 | // log to debug 41 | //console.log('onKeyChange', event, event.keyCode, event.shiftKey, event.ctrlKey, event.altKey, event.metaKey) 42 | 43 | // update this.keyCodes 44 | const keyCode = event.keyCode; 45 | this.keyCodes[keyCode] = event.type === 'keydown'; 46 | } 47 | 48 | pressed(keyDesc) { 49 | const keys = keyDesc.split('+'); 50 | for(let i = 0; i < keys.length; i++) { 51 | const key = keys[i]; 52 | let pressed = false; 53 | if(Object.keys(ALIAS).indexOf(key) != -1) { 54 | pressed = this.keyCodes[ALIAS[key]]; 55 | } else { 56 | pressed = this.keyCodes[key.toUpperCase().charCodeAt(0)]; 57 | } 58 | if(!pressed) 59 | return false; 60 | } 61 | 62 | return true; 63 | } 64 | 65 | eventMatches(event, keyDesc) { 66 | const aliases = ALIAS; 67 | const aliasKeys = Object.keys(aliases); 68 | const keys = keyDesc.split('+'); 69 | // log to debug 70 | // console.log('eventMatches', event, event.keyCode, event.shiftKey, event.ctrlKey, event.altKey, event.metaKey) 71 | for(let i = 0; i < keys.length; i++) { 72 | const key = keys[i]; 73 | let pressed = false; 74 | if(key === 'shift') { 75 | pressed = event.shiftKey ? true : false; 76 | } else if(key === 'ctrl') { 77 | pressed = event.ctrlKey ? true : false; 78 | } else if(key === 'alt') { 79 | pressed = event.altKey ? true : false; 80 | } else if(key === 'meta') { 81 | pressed = event.metaKey ? true : false; 82 | } else if(aliasKeys.indexOf(key) !== -1) { 83 | pressed = event.keyCode === aliases[key]; 84 | } else if(event.keyCode === key.toUpperCase().charCodeAt(0)) { 85 | pressed = true; 86 | } 87 | if(!pressed) 88 | return false; 89 | } 90 | 91 | return true; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/js/utils/objloader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author mrdoob / http://mrdoob.com/ 3 | */ 4 | module.exports = function (THREE) { 5 | THREE.OBJLoader = function ( manager ) { 6 | 7 | this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager; 8 | 9 | this.materials = null; 10 | 11 | this.regexp = { 12 | // v float float float 13 | vertex_pattern : /^v\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/, 14 | // vn float float float 15 | normal_pattern : /^vn\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/, 16 | // vt float float 17 | uv_pattern : /^vt\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/, 18 | // f vertex vertex vertex 19 | face_vertex : /^f\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)(?:\s+(-?\d+))?/, 20 | // f vertex/uv vertex/uv vertex/uv 21 | face_vertex_uv : /^f\s+(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)(?:\s+(-?\d+)\/(-?\d+))?/, 22 | // f vertex/uv/normal vertex/uv/normal vertex/uv/normal 23 | face_vertex_uv_normal : /^f\s+(-?\d+)\/(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\/(-?\d+)(?:\s+(-?\d+)\/(-?\d+)\/(-?\d+))?/, 24 | // f vertex//normal vertex//normal vertex//normal 25 | face_vertex_normal : /^f\s+(-?\d+)\/\/(-?\d+)\s+(-?\d+)\/\/(-?\d+)\s+(-?\d+)\/\/(-?\d+)(?:\s+(-?\d+)\/\/(-?\d+))?/, 26 | // o object_name | g group_name 27 | object_pattern : /^[og]\s*(.+)?/, 28 | // s boolean 29 | smoothing_pattern : /^s\s+(\d+|on|off)/, 30 | // mtllib file_reference 31 | material_library_pattern : /^mtllib /, 32 | // usemtl material_name 33 | material_use_pattern : /^usemtl / 34 | }; 35 | 36 | }; 37 | 38 | THREE.OBJLoader.prototype = { 39 | 40 | constructor: THREE.OBJLoader, 41 | 42 | load: function ( url, onLoad, onProgress, onError ) { 43 | 44 | var scope = this; 45 | 46 | var loader = new THREE.FileLoader( scope.manager ); 47 | loader.setPath( this.path ); 48 | loader.load( url, function ( text ) { 49 | 50 | onLoad( scope.parse( text ) ); 51 | 52 | }, onProgress, onError ); 53 | 54 | }, 55 | 56 | setPath: function ( value ) { 57 | 58 | this.path = value; 59 | 60 | }, 61 | 62 | setMaterials: function ( materials ) { 63 | 64 | this.materials = materials; 65 | 66 | }, 67 | 68 | _createParserState : function () { 69 | 70 | var state = { 71 | objects : [], 72 | object : {}, 73 | 74 | vertices : [], 75 | normals : [], 76 | uvs : [], 77 | 78 | materialLibraries : [], 79 | 80 | startObject: function ( name, fromDeclaration ) { 81 | 82 | // If the current object (initial from reset) is not from a g/o declaration in the parsed 83 | // file. We need to use it for the first parsed g/o to keep things in sync. 84 | if ( this.object && this.object.fromDeclaration === false ) { 85 | 86 | this.object.name = name; 87 | this.object.fromDeclaration = ( fromDeclaration !== false ); 88 | return; 89 | 90 | } 91 | 92 | var previousMaterial = ( this.object && typeof this.object.currentMaterial === 'function' ? this.object.currentMaterial() : undefined ); 93 | 94 | if ( this.object && typeof this.object._finalize === 'function' ) { 95 | 96 | this.object._finalize( true ); 97 | 98 | } 99 | 100 | this.object = { 101 | name : name || '', 102 | fromDeclaration : ( fromDeclaration !== false ), 103 | 104 | geometry : { 105 | vertices : [], 106 | normals : [], 107 | uvs : [] 108 | }, 109 | materials : [], 110 | smooth : true, 111 | 112 | startMaterial : function( name, libraries ) { 113 | 114 | var previous = this._finalize( false ); 115 | 116 | // New usemtl declaration overwrites an inherited material, except if faces were declared 117 | // after the material, then it must be preserved for proper MultiMaterial continuation. 118 | if ( previous && ( previous.inherited || previous.groupCount <= 0 ) ) { 119 | 120 | this.materials.splice( previous.index, 1 ); 121 | 122 | } 123 | 124 | var material = { 125 | index : this.materials.length, 126 | name : name || '', 127 | mtllib : ( Array.isArray( libraries ) && libraries.length > 0 ? libraries[ libraries.length - 1 ] : '' ), 128 | smooth : ( previous !== undefined ? previous.smooth : this.smooth ), 129 | groupStart : ( previous !== undefined ? previous.groupEnd : 0 ), 130 | groupEnd : -1, 131 | groupCount : -1, 132 | inherited : false, 133 | 134 | clone : function( index ) { 135 | var cloned = { 136 | index : ( typeof index === 'number' ? index : this.index ), 137 | name : this.name, 138 | mtllib : this.mtllib, 139 | smooth : this.smooth, 140 | groupStart : 0, 141 | groupEnd : -1, 142 | groupCount : -1, 143 | inherited : false 144 | }; 145 | cloned.clone = this.clone.bind(cloned); 146 | return cloned; 147 | } 148 | }; 149 | 150 | this.materials.push( material ); 151 | 152 | return material; 153 | 154 | }, 155 | 156 | currentMaterial : function() { 157 | 158 | if ( this.materials.length > 0 ) { 159 | return this.materials[ this.materials.length - 1 ]; 160 | } 161 | 162 | return undefined; 163 | 164 | }, 165 | 166 | _finalize : function( end ) { 167 | 168 | var lastMultiMaterial = this.currentMaterial(); 169 | if ( lastMultiMaterial && lastMultiMaterial.groupEnd === -1 ) { 170 | 171 | lastMultiMaterial.groupEnd = this.geometry.vertices.length / 3; 172 | lastMultiMaterial.groupCount = lastMultiMaterial.groupEnd - lastMultiMaterial.groupStart; 173 | lastMultiMaterial.inherited = false; 174 | 175 | } 176 | 177 | // Ignore objects tail materials if no face declarations followed them before a new o/g started. 178 | if ( end && this.materials.length > 1 ) { 179 | 180 | for ( var mi = this.materials.length - 1; mi >= 0; mi-- ) { 181 | if ( this.materials[mi].groupCount <= 0 ) { 182 | this.materials.splice( mi, 1 ); 183 | } 184 | } 185 | 186 | } 187 | 188 | // Guarantee at least one empty material, this makes the creation later more straight forward. 189 | if ( end && this.materials.length === 0 ) { 190 | 191 | this.materials.push({ 192 | name : '', 193 | smooth : this.smooth 194 | }); 195 | 196 | } 197 | 198 | return lastMultiMaterial; 199 | 200 | } 201 | }; 202 | 203 | // Inherit previous objects material. 204 | // Spec tells us that a declared material must be set to all objects until a new material is declared. 205 | // If a usemtl declaration is encountered while this new object is being parsed, it will 206 | // overwrite the inherited material. Exception being that there was already face declarations 207 | // to the inherited material, then it will be preserved for proper MultiMaterial continuation. 208 | 209 | if ( previousMaterial && previousMaterial.name && typeof previousMaterial.clone === "function" ) { 210 | 211 | var declared = previousMaterial.clone( 0 ); 212 | declared.inherited = true; 213 | this.object.materials.push( declared ); 214 | 215 | } 216 | 217 | this.objects.push( this.object ); 218 | 219 | }, 220 | 221 | finalize : function() { 222 | 223 | if ( this.object && typeof this.object._finalize === 'function' ) { 224 | 225 | this.object._finalize( true ); 226 | 227 | } 228 | 229 | }, 230 | 231 | parseVertexIndex: function ( value, len ) { 232 | 233 | var index = parseInt( value, 10 ); 234 | return ( index >= 0 ? index - 1 : index + len / 3 ) * 3; 235 | 236 | }, 237 | 238 | parseNormalIndex: function ( value, len ) { 239 | 240 | var index = parseInt( value, 10 ); 241 | return ( index >= 0 ? index - 1 : index + len / 3 ) * 3; 242 | 243 | }, 244 | 245 | parseUVIndex: function ( value, len ) { 246 | 247 | var index = parseInt( value, 10 ); 248 | return ( index >= 0 ? index - 1 : index + len / 2 ) * 2; 249 | 250 | }, 251 | 252 | addVertex: function ( a, b, c ) { 253 | 254 | var src = this.vertices; 255 | var dst = this.object.geometry.vertices; 256 | 257 | dst.push( src[ a + 0 ] ); 258 | dst.push( src[ a + 1 ] ); 259 | dst.push( src[ a + 2 ] ); 260 | dst.push( src[ b + 0 ] ); 261 | dst.push( src[ b + 1 ] ); 262 | dst.push( src[ b + 2 ] ); 263 | dst.push( src[ c + 0 ] ); 264 | dst.push( src[ c + 1 ] ); 265 | dst.push( src[ c + 2 ] ); 266 | 267 | }, 268 | 269 | addVertexLine: function ( a ) { 270 | 271 | var src = this.vertices; 272 | var dst = this.object.geometry.vertices; 273 | 274 | dst.push( src[ a + 0 ] ); 275 | dst.push( src[ a + 1 ] ); 276 | dst.push( src[ a + 2 ] ); 277 | 278 | }, 279 | 280 | addNormal : function ( a, b, c ) { 281 | 282 | var src = this.normals; 283 | var dst = this.object.geometry.normals; 284 | 285 | dst.push( src[ a + 0 ] ); 286 | dst.push( src[ a + 1 ] ); 287 | dst.push( src[ a + 2 ] ); 288 | dst.push( src[ b + 0 ] ); 289 | dst.push( src[ b + 1 ] ); 290 | dst.push( src[ b + 2 ] ); 291 | dst.push( src[ c + 0 ] ); 292 | dst.push( src[ c + 1 ] ); 293 | dst.push( src[ c + 2 ] ); 294 | 295 | }, 296 | 297 | addUV: function ( a, b, c ) { 298 | 299 | var src = this.uvs; 300 | var dst = this.object.geometry.uvs; 301 | 302 | dst.push( src[ a + 0 ] ); 303 | dst.push( src[ a + 1 ] ); 304 | dst.push( src[ b + 0 ] ); 305 | dst.push( src[ b + 1 ] ); 306 | dst.push( src[ c + 0 ] ); 307 | dst.push( src[ c + 1 ] ); 308 | 309 | }, 310 | 311 | addUVLine: function ( a ) { 312 | 313 | var src = this.uvs; 314 | var dst = this.object.geometry.uvs; 315 | 316 | dst.push( src[ a + 0 ] ); 317 | dst.push( src[ a + 1 ] ); 318 | 319 | }, 320 | 321 | addFace: function ( a, b, c, d, ua, ub, uc, ud, na, nb, nc, nd ) { 322 | 323 | var vLen = this.vertices.length; 324 | 325 | var ia = this.parseVertexIndex( a, vLen ); 326 | var ib = this.parseVertexIndex( b, vLen ); 327 | var ic = this.parseVertexIndex( c, vLen ); 328 | var id; 329 | 330 | if ( d === undefined ) { 331 | 332 | this.addVertex( ia, ib, ic ); 333 | 334 | } else { 335 | 336 | id = this.parseVertexIndex( d, vLen ); 337 | 338 | this.addVertex( ia, ib, id ); 339 | this.addVertex( ib, ic, id ); 340 | 341 | } 342 | 343 | if ( ua !== undefined ) { 344 | 345 | var uvLen = this.uvs.length; 346 | 347 | ia = this.parseUVIndex( ua, uvLen ); 348 | ib = this.parseUVIndex( ub, uvLen ); 349 | ic = this.parseUVIndex( uc, uvLen ); 350 | 351 | if ( d === undefined ) { 352 | 353 | this.addUV( ia, ib, ic ); 354 | 355 | } else { 356 | 357 | id = this.parseUVIndex( ud, uvLen ); 358 | 359 | this.addUV( ia, ib, id ); 360 | this.addUV( ib, ic, id ); 361 | 362 | } 363 | 364 | } 365 | 366 | if ( na !== undefined ) { 367 | 368 | // Normals are many times the same. If so, skip function call and parseInt. 369 | var nLen = this.normals.length; 370 | ia = this.parseNormalIndex( na, nLen ); 371 | 372 | ib = na === nb ? ia : this.parseNormalIndex( nb, nLen ); 373 | ic = na === nc ? ia : this.parseNormalIndex( nc, nLen ); 374 | 375 | if ( d === undefined ) { 376 | 377 | this.addNormal( ia, ib, ic ); 378 | 379 | } else { 380 | 381 | id = this.parseNormalIndex( nd, nLen ); 382 | 383 | this.addNormal( ia, ib, id ); 384 | this.addNormal( ib, ic, id ); 385 | 386 | } 387 | 388 | } 389 | 390 | }, 391 | 392 | addLineGeometry: function ( vertices, uvs ) { 393 | 394 | this.object.geometry.type = 'Line'; 395 | 396 | var vLen = this.vertices.length; 397 | var uvLen = this.uvs.length; 398 | 399 | for ( var vi = 0, l = vertices.length; vi < l; vi ++ ) { 400 | 401 | this.addVertexLine( this.parseVertexIndex( vertices[ vi ], vLen ) ); 402 | 403 | } 404 | 405 | for ( var uvi = 0, l = uvs.length; uvi < l; uvi ++ ) { 406 | 407 | this.addUVLine( this.parseUVIndex( uvs[ uvi ], uvLen ) ); 408 | 409 | } 410 | 411 | } 412 | 413 | }; 414 | 415 | state.startObject( '', false ); 416 | 417 | return state; 418 | 419 | }, 420 | 421 | parse: function ( text ) { 422 | 423 | console.time( 'OBJLoader' ); 424 | 425 | var state = this._createParserState(); 426 | 427 | if ( text.indexOf( '\r\n' ) !== - 1 ) { 428 | 429 | // This is faster than String.split with regex that splits on both 430 | text = text.replace( /\r\n/g, '\n' ); 431 | 432 | } 433 | 434 | if ( text.indexOf( '\\\n' ) !== - 1) { 435 | 436 | // join lines separated by a line continuation character (\) 437 | text = text.replace( /\\\n/g, '' ); 438 | 439 | } 440 | 441 | var lines = text.split( '\n' ); 442 | var line = '', lineFirstChar = '', lineSecondChar = ''; 443 | var lineLength = 0; 444 | var result = []; 445 | 446 | // Faster to just trim left side of the line. Use if available. 447 | var trimLeft = ( typeof ''.trimLeft === 'function' ); 448 | 449 | for ( var i = 0, l = lines.length; i < l; i ++ ) { 450 | 451 | line = lines[ i ]; 452 | 453 | line = trimLeft ? line.trimLeft() : line.trim(); 454 | 455 | lineLength = line.length; 456 | 457 | if ( lineLength === 0 ) continue; 458 | 459 | lineFirstChar = line.charAt( 0 ); 460 | 461 | // @todo invoke passed in handler if any 462 | if ( lineFirstChar === '#' ) continue; 463 | 464 | if ( lineFirstChar === 'v' ) { 465 | 466 | lineSecondChar = line.charAt( 1 ); 467 | 468 | if ( lineSecondChar === ' ' && ( result = this.regexp.vertex_pattern.exec( line ) ) !== null ) { 469 | 470 | // 0 1 2 3 471 | // ["v 1.0 2.0 3.0", "1.0", "2.0", "3.0"] 472 | 473 | state.vertices.push( 474 | parseFloat( result[ 1 ] ), 475 | parseFloat( result[ 2 ] ), 476 | parseFloat( result[ 3 ] ) 477 | ); 478 | 479 | } else if ( lineSecondChar === 'n' && ( result = this.regexp.normal_pattern.exec( line ) ) !== null ) { 480 | 481 | // 0 1 2 3 482 | // ["vn 1.0 2.0 3.0", "1.0", "2.0", "3.0"] 483 | 484 | state.normals.push( 485 | parseFloat( result[ 1 ] ), 486 | parseFloat( result[ 2 ] ), 487 | parseFloat( result[ 3 ] ) 488 | ); 489 | 490 | } else if ( lineSecondChar === 't' && ( result = this.regexp.uv_pattern.exec( line ) ) !== null ) { 491 | 492 | // 0 1 2 493 | // ["vt 0.1 0.2", "0.1", "0.2"] 494 | 495 | state.uvs.push( 496 | parseFloat( result[ 1 ] ), 497 | parseFloat( result[ 2 ] ) 498 | ); 499 | 500 | } else { 501 | 502 | throw new Error( "Unexpected vertex/normal/uv line: '" + line + "'" ); 503 | 504 | } 505 | 506 | } else if ( lineFirstChar === "f" ) { 507 | 508 | if ( ( result = this.regexp.face_vertex_uv_normal.exec( line ) ) !== null ) { 509 | 510 | // f vertex/uv/normal vertex/uv/normal vertex/uv/normal 511 | // 0 1 2 3 4 5 6 7 8 9 10 11 12 512 | // ["f 1/1/1 2/2/2 3/3/3", "1", "1", "1", "2", "2", "2", "3", "3", "3", undefined, undefined, undefined] 513 | 514 | state.addFace( 515 | result[ 1 ], result[ 4 ], result[ 7 ], result[ 10 ], 516 | result[ 2 ], result[ 5 ], result[ 8 ], result[ 11 ], 517 | result[ 3 ], result[ 6 ], result[ 9 ], result[ 12 ] 518 | ); 519 | 520 | } else if ( ( result = this.regexp.face_vertex_uv.exec( line ) ) !== null ) { 521 | 522 | // f vertex/uv vertex/uv vertex/uv 523 | // 0 1 2 3 4 5 6 7 8 524 | // ["f 1/1 2/2 3/3", "1", "1", "2", "2", "3", "3", undefined, undefined] 525 | 526 | state.addFace( 527 | result[ 1 ], result[ 3 ], result[ 5 ], result[ 7 ], 528 | result[ 2 ], result[ 4 ], result[ 6 ], result[ 8 ] 529 | ); 530 | 531 | } else if ( ( result = this.regexp.face_vertex_normal.exec( line ) ) !== null ) { 532 | 533 | // f vertex//normal vertex//normal vertex//normal 534 | // 0 1 2 3 4 5 6 7 8 535 | // ["f 1//1 2//2 3//3", "1", "1", "2", "2", "3", "3", undefined, undefined] 536 | 537 | state.addFace( 538 | result[ 1 ], result[ 3 ], result[ 5 ], result[ 7 ], 539 | undefined, undefined, undefined, undefined, 540 | result[ 2 ], result[ 4 ], result[ 6 ], result[ 8 ] 541 | ); 542 | 543 | } else if ( ( result = this.regexp.face_vertex.exec( line ) ) !== null ) { 544 | 545 | // f vertex vertex vertex 546 | // 0 1 2 3 4 547 | // ["f 1 2 3", "1", "2", "3", undefined] 548 | 549 | state.addFace( 550 | result[ 1 ], result[ 2 ], result[ 3 ], result[ 4 ] 551 | ); 552 | 553 | } else { 554 | 555 | throw new Error( "Unexpected face line: '" + line + "'" ); 556 | 557 | } 558 | 559 | } else if ( lineFirstChar === "l" ) { 560 | 561 | var lineParts = line.substring( 1 ).trim().split( " " ); 562 | var lineVertices = [], lineUVs = []; 563 | 564 | if ( line.indexOf( "/" ) === - 1 ) { 565 | 566 | lineVertices = lineParts; 567 | 568 | } else { 569 | 570 | for ( var li = 0, llen = lineParts.length; li < llen; li ++ ) { 571 | 572 | var parts = lineParts[ li ].split( "/" ); 573 | 574 | if ( parts[ 0 ] !== "" ) lineVertices.push( parts[ 0 ] ); 575 | if ( parts[ 1 ] !== "" ) lineUVs.push( parts[ 1 ] ); 576 | 577 | } 578 | 579 | } 580 | state.addLineGeometry( lineVertices, lineUVs ); 581 | 582 | } else if ( ( result = this.regexp.object_pattern.exec( line ) ) !== null ) { 583 | 584 | // o object_name 585 | // or 586 | // g group_name 587 | 588 | // WORKAROUND: https://bugs.chromium.org/p/v8/issues/detail?id=2869 589 | // var name = result[ 0 ].substr( 1 ).trim(); 590 | var name = ( " " + result[ 0 ].substr( 1 ).trim() ).substr( 1 ); 591 | 592 | state.startObject( name ); 593 | 594 | } else if ( this.regexp.material_use_pattern.test( line ) ) { 595 | 596 | // material 597 | 598 | state.object.startMaterial( line.substring( 7 ).trim(), state.materialLibraries ); 599 | 600 | } else if ( this.regexp.material_library_pattern.test( line ) ) { 601 | 602 | // mtl file 603 | 604 | state.materialLibraries.push( line.substring( 7 ).trim() ); 605 | 606 | } else if ( ( result = this.regexp.smoothing_pattern.exec( line ) ) !== null ) { 607 | 608 | // smooth shading 609 | 610 | // @todo Handle files that have varying smooth values for a set of faces inside one geometry, 611 | // but does not define a usemtl for each face set. 612 | // This should be detected and a dummy material created (later MultiMaterial and geometry groups). 613 | // This requires some care to not create extra material on each smooth value for "normal" obj files. 614 | // where explicit usemtl defines geometry groups. 615 | // Example asset: examples/models/obj/cerberus/Cerberus.obj 616 | 617 | var value = result[ 1 ].trim().toLowerCase(); 618 | state.object.smooth = ( value === '1' || value === 'on' ); 619 | 620 | var material = state.object.currentMaterial(); 621 | if ( material ) { 622 | 623 | material.smooth = state.object.smooth; 624 | 625 | } 626 | 627 | } else { 628 | 629 | // Handle null terminated files without exception 630 | if ( line === '\0' ) continue; 631 | 632 | throw new Error( "Unexpected line: '" + line + "'" ); 633 | 634 | } 635 | 636 | } 637 | 638 | state.finalize(); 639 | 640 | var container = new THREE.Group(); 641 | container.materialLibraries = [].concat( state.materialLibraries ); 642 | 643 | for ( var i = 0, l = state.objects.length; i < l; i ++ ) { 644 | 645 | var object = state.objects[ i ]; 646 | var geometry = object.geometry; 647 | var materials = object.materials; 648 | var isLine = ( geometry.type === 'Line' ); 649 | 650 | // Skip o/g line declarations that did not follow with any faces 651 | if ( geometry.vertices.length === 0 ) continue; 652 | 653 | var buffergeometry = new THREE.BufferGeometry(); 654 | 655 | buffergeometry.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array( geometry.vertices ), 3 ) ); 656 | 657 | if ( geometry.normals.length > 0 ) { 658 | 659 | buffergeometry.addAttribute( 'normal', new THREE.BufferAttribute( new Float32Array( geometry.normals ), 3 ) ); 660 | 661 | } else { 662 | 663 | buffergeometry.computeVertexNormals(); 664 | 665 | } 666 | 667 | if ( geometry.uvs.length > 0 ) { 668 | 669 | buffergeometry.addAttribute( 'uv', new THREE.BufferAttribute( new Float32Array( geometry.uvs ), 2 ) ); 670 | 671 | } 672 | 673 | // Create materials 674 | 675 | var createdMaterials = []; 676 | 677 | for ( var mi = 0, miLen = materials.length; mi < miLen ; mi++ ) { 678 | 679 | var sourceMaterial = materials[mi]; 680 | var material = undefined; 681 | 682 | if ( this.materials !== null ) { 683 | 684 | material = this.materials.create( sourceMaterial.name ); 685 | 686 | // mtl etc. loaders probably can't create line materials correctly, copy properties to a line material. 687 | if ( isLine && material && ! ( material instanceof THREE.LineBasicMaterial ) ) { 688 | 689 | var materialLine = new THREE.LineBasicMaterial(); 690 | materialLine.copy( material ); 691 | material = materialLine; 692 | 693 | } 694 | 695 | } 696 | 697 | if ( ! material ) { 698 | 699 | material = ( ! isLine ? new THREE.MeshPhongMaterial() : new THREE.LineBasicMaterial() ); 700 | material.name = sourceMaterial.name; 701 | 702 | } 703 | 704 | material.shading = sourceMaterial.smooth ? THREE.SmoothShading : THREE.FlatShading; 705 | 706 | createdMaterials.push(material); 707 | 708 | } 709 | 710 | // Create mesh 711 | 712 | var mesh; 713 | 714 | if ( createdMaterials.length > 1 ) { 715 | 716 | for ( var mi = 0, miLen = materials.length; mi < miLen ; mi++ ) { 717 | 718 | var sourceMaterial = materials[mi]; 719 | buffergeometry.addGroup( sourceMaterial.groupStart, sourceMaterial.groupCount, mi ); 720 | 721 | } 722 | 723 | var multiMaterial = new THREE.MultiMaterial( createdMaterials ); 724 | mesh = ( ! isLine ? new THREE.Mesh( buffergeometry, multiMaterial ) : new THREE.LineSegments( buffergeometry, multiMaterial ) ); 725 | 726 | } else { 727 | 728 | mesh = ( ! isLine ? new THREE.Mesh( buffergeometry, createdMaterials[ 0 ] ) : new THREE.LineSegments( buffergeometry, createdMaterials[ 0 ] ) ); 729 | } 730 | 731 | mesh.name = object.name; 732 | 733 | container.add( mesh ); 734 | 735 | } 736 | 737 | console.timeEnd( 'OBJLoader' ); 738 | 739 | return container; 740 | 741 | } 742 | 743 | }; 744 | } 745 | -------------------------------------------------------------------------------- /src/public/assets/css/app.css: -------------------------------------------------------------------------------- 1 | html { 2 | -ms-text-size-adjust: 100%; 3 | -webkit-text-size-adjust: 100%; } 4 | 5 | body { 6 | margin: 0; 7 | font: 16px/1 sans-serif; 8 | -moz-osx-font-smoothing: grayscale; 9 | -webkit-font-smoothing: antialiased; } 10 | 11 | h1, 12 | h2, 13 | h3, 14 | h4, 15 | p, 16 | blockquote, 17 | figure, 18 | ol, 19 | ul { 20 | margin: 0; 21 | padding: 0; } 22 | 23 | main, 24 | li { 25 | display: block; } 26 | 27 | h1, 28 | h2, 29 | h3, 30 | h4 { 31 | font-size: inherit; } 32 | 33 | strong { 34 | font-weight: bold; } 35 | 36 | a, 37 | button { 38 | color: inherit; 39 | transition: .3s; } 40 | 41 | a { 42 | text-decoration: none; } 43 | 44 | button { 45 | overflow: visible; 46 | border: 0; 47 | font: inherit; 48 | -webkit-font-smoothing: inherit; 49 | letter-spacing: inherit; 50 | background: none; 51 | cursor: pointer; } 52 | 53 | ::-moz-focus-inner { 54 | padding: 0; 55 | border: 0; } 56 | 57 | :focus { 58 | outline: 0; } 59 | 60 | img { 61 | max-width: 100%; 62 | height: auto; 63 | border: 0; } 64 | 65 | body { 66 | overflow: hidden; } 67 | 68 | .main { 69 | position: relative; 70 | width: 100%; 71 | height: 100vh; } 72 | 73 | #loading { 74 | font-family: roboto,'helvetica neue', helvetica, arial, sans-serif; 75 | position: absolute; 76 | top: calc(50% - 20px); 77 | left: calc(50% - 35px); } 78 | 79 | #UIContainer { 80 | position: absolute; 81 | right: 200px; 82 | top: 0%; 83 | column-count: 1; } 84 | 85 | #add-object-button { 86 | z-index: 1; 87 | position: relative; 88 | margin: 0; 89 | margin-top: -2px; 90 | float: left; 91 | color: white; 92 | border: 0px; 93 | display: inline-block; 94 | padding: 0px 10px; 95 | padding-bottom: 4px; 96 | cursor: pointer; 97 | background-color: #1177ff; 98 | font-size: 30px; } 99 | 100 | #add-object-button.active { 101 | background-color: #f44b76; } 102 | 103 | #guis { 104 | position: absolute; 105 | top: 40px; 106 | width: 220px; 107 | background-color: rgba(255, 255, 255, 0.6); 108 | font-family: roboto, 'helvetica neue', helvetica, arial, sans-serif; 109 | line-height: 20px; 110 | box-shadow: 0 0 5px #aaa; 111 | transition: background-color 200ms ease, color 200ms ease, opacity 200ms ease; 112 | -webkit-transition: background-color 200ms ease, color 200ms ease, opacity 200ms ease; } 113 | 114 | #guis > div { 115 | padding: 15px; 116 | position: relative; } 117 | 118 | #guis > div + div { 119 | border-top: 0.5px solid #aaa; } 120 | 121 | #guis h4 { 122 | position: relative; 123 | margin: 0; 124 | margin-top: 0.2em; 125 | font-weight: 400; 126 | text-align: center; } 127 | 128 | #guis h4 + * { 129 | margin-top: 1em; } 130 | 131 | #guis div > span { 132 | font-size: 0.8em; 133 | vertical-align: top; } 134 | 135 | #guis .property { 136 | font-weight: bold; 137 | display: inline-block; 138 | width: 70px; 139 | position: absolute; } 140 | 141 | #guis .valueSpan { 142 | cursor: pointer; 143 | border-bottom: 1px dotted #555; 144 | display: inline-block; 145 | max-width: 90px; 146 | overflow: hidden; 147 | text-overflow: ellipsis; } 148 | 149 | #guis { 150 | -webkit-touch-callout: none; 151 | /* iOS Safari */ 152 | -webkit-user-select: none; 153 | /* Chrome/Safari/Opera */ 154 | -khtml-user-select: none; 155 | /* Konqueror */ 156 | -moz-user-select: none; 157 | /* Firefox */ 158 | -ms-user-select: none; 159 | /* Internet Explorer/Edge */ 160 | user-select: none; 161 | /* Non-prefixed version, currently 162 | not supported by any browser */ } 163 | 164 | .property + .valueSpan { 165 | margin-left: 80px; } 166 | 167 | #guis.editor { 168 | background-color: #555; 169 | color: white; } 170 | 171 | #guis.editor .valueSpan { 172 | border-bottom-color: white; } 173 | 174 | #guis .edit-toggle { 175 | line-height: 1.2em; 176 | color: indigo; } 177 | 178 | #guis.editor .edit-toggle { 179 | color: skyblue; } 180 | 181 | #guis .remove-file { 182 | padding: 0 2px; 183 | margin: 0 2px; 184 | color: #f44b76; 185 | cursor: pointer; 186 | font-size: 20px; 187 | line-height: 11px; 188 | display: inline-block; 189 | vertical-align: top; 190 | font-weight: bold; } 191 | 192 | #guis .remove-file:hover { 193 | color: pink; } 194 | 195 | #guis .cone, 196 | #guis.editor .cone { 197 | color: black; } 198 | 199 | #guis .cone .valueSpan, 200 | #guis.editor .cone .valueSpan { 201 | border-bottom-color: #555; } 202 | 203 | #guis.editor .nav-object { 204 | color: rgba(220, 220, 220, 0.5); } 205 | 206 | #guis.editor .nav-object:hover { 207 | color: white; } 208 | 209 | #guis .nav { 210 | color: rgba(85, 85, 85, 0.5); 211 | transition: color 100ms ease; 212 | top: 10px; 213 | cursor: pointer; 214 | padding: 0 12px; 215 | font-size: 40px; 216 | position: absolute; 217 | line-height: 1.1em; } 218 | 219 | #guis .nav:hover { 220 | color: #333; 221 | text-shadow: 0 0 5px #aaa; } 222 | 223 | #guis .nav-left { 224 | left: 0; } 225 | 226 | #guis .nav-right { 227 | right: 0; } 228 | 229 | #guis .active { 230 | background-color: #99ccff; } 231 | 232 | input[type='file'] { 233 | display: none; } 234 | 235 | #camera-label { 236 | z-index: 1; 237 | position: absolute; 238 | bottom: 25px; 239 | width: 100%; 240 | text-align: center; 241 | font-size: 18px; 242 | text-decoration: underline; 243 | text-decoration-style: dotted; 244 | -moz-text-decoration-style: dotted; 245 | -webkit-text-decoration-style: dotted; 246 | text-decoration-style: dotted; 247 | color: #555; 248 | cursor: pointer; } 249 | 250 | #play-mute-button { 251 | position: absolute; 252 | top: 10px; 253 | left: 15px; } 254 | 255 | #play-mute-button img { 256 | cursor: pointer; 257 | width: 20px; 258 | height: 20px; 259 | opacity: 0.5; } 260 | 261 | #play-mute-button img:hover { 262 | opacity: 0.66; } 263 | 264 | #unmute-button { 265 | display: none; } 266 | 267 | #save { 268 | position: absolute; 269 | bottom: 10px; 270 | left: 15px; } 271 | 272 | #save img { 273 | cursor: pointer; 274 | width: 15px; 275 | height: 15px; 276 | opacity: 0.5; } 277 | 278 | #load { 279 | z-index: 2; 280 | position: absolute; 281 | bottom: 10px; 282 | left: 45px; } 283 | 284 | #load img { 285 | cursor: pointer; 286 | width: 15px; 287 | height: 15px; 288 | opacity: 0.67; } 289 | 290 | #githubLink { 291 | z-index: 2; 292 | position: absolute; 293 | bottom: 10px; 294 | right: 15px; 295 | width: 25px; 296 | height: 25px; 297 | opacity: 0.7; } 298 | 299 | .help-bubble { 300 | float: left; 301 | position: absolute; 302 | padding: 10px; 303 | color: #fff; 304 | background: #777; 305 | opacity: 0.75; 306 | -webkit-border-radius: 10px; 307 | -moz-border-radius: 10px; 308 | border-radius: 10px; 309 | display: block; 310 | font-size: 12px; 311 | font-family: roboto, 'helvetica neue', helvetica, arial, sans-serif; 312 | line-height: 1.25em; 313 | pointer-events: none; } 314 | -------------------------------------------------------------------------------- /src/public/assets/css/rStats.css: -------------------------------------------------------------------------------- 1 | .alarm { 2 | color: #b70000; 3 | text-shadow: 0 0 0 #b70000, 4 | 0 0 1px #ffffff, 5 | 0 0 1px #ffffff, 6 | 0 0 2px #ffffff, 7 | 0 0 2px #ffffff, 8 | 0 0 3px #ffffff, 9 | 0 0 3px #ffffff, 10 | 0 0 4px #ffffff, 11 | 0 0 4px #ffffff; 12 | } 13 | 14 | .rs-base { 15 | position: absolute; 16 | top: 0; 17 | left: 0; 18 | z-index: 10000; 19 | overflow: hidden; 20 | padding: 10px; 21 | width: 350px; 22 | background-color: #222222; 23 | font-size: 10px; 24 | font-family: 'Roboto Condensed', tahoma, sans-serif; 25 | line-height: 1.2em; 26 | display: none; 27 | } 28 | 29 | .rs-base h1 { 30 | margin: 0; 31 | margin-bottom: 5px; 32 | padding: 0; 33 | color: #ffffff; 34 | font-size: 1.4em; 35 | cursor: pointer; 36 | } 37 | 38 | .rs-base div.rs-group { 39 | margin-bottom: 10px; 40 | } 41 | 42 | .rs-base div.rs-group.hidden { 43 | display: none; 44 | } 45 | 46 | .rs-base div.rs-fraction { 47 | position: relative; 48 | margin-bottom: 5px; 49 | } 50 | 51 | .rs-base div.rs-fraction p { 52 | margin: 0; 53 | padding: 0; 54 | width: 120px; 55 | text-align: right; 56 | } 57 | 58 | .rs-base div.rs-legend { 59 | position: absolute; 60 | line-height: 1em; 61 | } 62 | 63 | .rs-base div.rs-counter-base { 64 | position: relative; 65 | margin: 2px 0; 66 | height: 1em; 67 | } 68 | 69 | .rs-base span.rs-counter-id { 70 | position: absolute; 71 | top: 0; 72 | left: 0; 73 | } 74 | 75 | .rs-base div.rs-counter-value { 76 | position: absolute; 77 | top: 0; 78 | left: 90px; 79 | width: 30px; 80 | height: 1em; 81 | text-align: right; 82 | } 83 | 84 | .rs-base canvas.rs-canvas { 85 | position: absolute; 86 | right: 0; 87 | } 88 | -------------------------------------------------------------------------------- /src/public/assets/js/libtess.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2000, Silicon Graphics, Inc. All Rights Reserved. 4 | Copyright 2015, Google Inc. All Rights Reserved. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to 8 | deal in the Software without restriction, including without limitation the 9 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 10 | sell copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice including the dates of first publication and 14 | either this permission notice or a reference to http://oss.sgi.com/projects/FreeB/ 15 | shall be included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 20 | SILICON GRAPHICS, INC. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | Original Code. The Original Code is: OpenGL Sample Implementation, 25 | Version 1.2.1, released January 26, 2000, developed by Silicon Graphics, 26 | Inc. The Original Code is Copyright (c) 1991-2000 Silicon Graphics, Inc. 27 | Copyright in any portions created by third parties is as indicated 28 | elsewhere herein. All Rights Reserved. 29 | */ 30 | 'use strict';var n;function t(a,b){return a.b===b.b&&a.a===b.a}function u(a,b){return a.ba?0:a;c=0>c?0:c;return a<=c?0===c?(b+d)/2:b+a/(a+c)*(d-b):d+c/(a+c)*(b-d)};function ea(a){var b=B(a.b);C(b,a.c);C(b.b,a.c);D(b,a.a);return b}function E(a,b){var c=!1,d=!1;a!==b&&(b.a!==a.a&&(d=!0,F(b.a,a.a)),b.d!==a.d&&(c=!0,G(b.d,a.d)),H(b,a),d||(C(b,a.a),a.a.c=a),c||(D(b,a.d),a.d.a=a))}function I(a){var b=a.b,c=!1;a.d!==a.b.d&&(c=!0,G(a.d,a.b.d));a.c===a?F(a.a,null):(a.b.d.a=J(a),a.a.c=a.c,H(a,J(a)),c||D(a,a.d));b.c===b?(F(b.a,null),G(b.d,null)):(a.d.a=J(b),b.a.c=b.c,H(b,J(b)));fa(a)} 32 | function K(a){var b=B(a),c=b.b;H(b,a.e);b.a=a.b.a;C(c,b.a);b.d=c.d=a.d;b=b.b;H(a.b,J(a.b));H(a.b,b);a.b.a=b.a;b.b.a.c=b.b;b.b.d=a.b.d;b.f=a.f;b.b.f=a.b.f;return b}function L(a,b){var c=!1,d=B(a),e=d.b;b.d!==a.d&&(c=!0,G(b.d,a.d));H(d,a.e);H(e,b);d.a=a.b.a;e.a=b.a;d.d=e.d=a.d;a.d.a=e;c||D(d,a.d);return d}function B(a){var b=new M,c=new M,d=a.b.h;c.h=d;d.b.h=b;b.h=a;a.b.h=c;b.b=c;b.c=b;b.e=c;c.b=b;c.c=c;return c.e=b}function H(a,b){var c=a.c,d=b.c;c.b.e=b;d.b.e=a;a.c=d;b.c=c} 33 | function C(a,b){var c=b.f,d=new N(b,c);c.e=d;b.f=d;c=d.c=a;do c.a=d,c=c.c;while(c!==a)}function D(a,b){var c=b.d,d=new ga(b,c);c.b=d;b.d=d;d.a=a;d.c=b.c;c=a;do c.d=d,c=c.e;while(c!==a)}function fa(a){var b=a.h;a=a.b.h;b.b.h=a;a.b.h=b}function F(a,b){var c=a.c,d=c;do d.a=b,d=d.c;while(d!==c);c=a.f;d=a.e;d.f=c;c.e=d}function G(a,b){var c=a.a,d=c;do d.d=b,d=d.e;while(d!==c);c=a.d;d=a.b;d.d=c;c.b=d};function ha(a){var b=0;Math.abs(a[1])>Math.abs(a[0])&&(b=1);Math.abs(a[2])>Math.abs(a[b])&&(b=2);return b};var O=4*1E150;function P(a,b){a.f+=b.f;a.b.f+=b.b.f}function ia(a,b,c){a=a.a;b=b.a;c=c.a;if(b.b.a===a)return c.b.a===a?u(b.a,c.a)?0>=x(c.b.a,b.a,c.a):0<=x(b.b.a,c.a,b.a):0>=x(c.b.a,a,c.a);if(c.b.a===a)return 0<=x(b.b.a,a,b.a);b=v(b.b.a,a,b.a);a=v(c.b.a,a,c.a);return b>=a}function Q(a){a.a.i=null;var b=a.e;b.a.c=b.c;b.c.a=b.a;a.e=null}function ja(a,b){I(a.a);a.c=!1;a.a=b;b.i=a}function ka(a){var b=a.a.a;do a=R(a);while(a.a.a===b);a.c&&(b=L(S(a).a.b,a.a.e),ja(a,b),a=R(a));return a} 34 | function la(a,b,c){var d=new ma;d.a=c;d.e=na(a.f,b.e,d);return c.i=d}function oa(a,b){switch(a.s){case 100130:return 0!==(b&1);case 100131:return 0!==b;case 100132:return 0b;case 100134:return 2<=b||-2>=b}return!1}function pa(a){var b=a.a,c=b.d;c.c=a.d;c.a=b;Q(a)}function T(a,b,c){a=b;for(b=b.a;a!==c;){a.c=!1;var d=S(a),e=d.a;if(e.a!==b.a){if(!d.c){pa(a);break}e=L(b.c.b,e.b);ja(d,e)}b.c!==e&&(E(J(e),e),E(b,e));pa(a);b=d.a;a=d}return b} 35 | function U(a,b,c,d,e,f){var g=!0;do la(a,b,c.b),c=c.c;while(c!==d);for(null===e&&(e=S(b).a.b.c);;){d=S(b);c=d.a.b;if(c.a!==e.a)break;c.c!==e&&(E(J(c),c),E(J(e),c));d.f=b.f-c.f;d.d=oa(a,d.f);b.b=!0;!g&&qa(a,b)&&(P(c,e),Q(b),I(e));g=!1;b=d;e=c}b.b=!0;f&&ra(a,b)}function sa(a,b,c,d,e){var f=[b.g[0],b.g[1],b.g[2]];b.d=null;b.d=a.o?a.o(f,c,d,a.c)||null:null;null===b.d&&(e?a.n||(V(a,100156),a.n=!0):b.d=c[0])} 36 | function ta(a,b,c){var d=[null,null,null,null];d[0]=b.a.d;d[1]=c.a.d;sa(a,b.a,d,[.5,.5,0,0],!1);E(b,c)}function ua(a,b,c,d,e){var f=Math.abs(b.b-a.b)+Math.abs(b.a-a.a),g=Math.abs(c.b-a.b)+Math.abs(c.a-a.a),h=e+1;d[e]=.5*g/(f+g);d[h]=.5*f/(f+g);a.g[0]+=d[e]*b.g[0]+d[h]*c.g[0];a.g[1]+=d[e]*b.g[1]+d[h]*c.g[1];a.g[2]+=d[e]*b.g[2]+d[h]*c.g[2]} 37 | function qa(a,b){var c=S(b),d=b.a,e=c.a;if(u(d.a,e.a)){if(0=l?W(c,l):u(h[g[l>>1]],h[g[l]])?W(c,l):va(c,l));h[f]=null;k[f]=c.b;c.b=f}else for(c.c[-(f+1)]=null;0x(d.b.a,e.a,d.a))return!1;R(b).b=b.b=!0;K(d.b);E(J(e),d)}return!0} 38 | function wa(a,b){var c=S(b),d=b.a,e=c.a,f=d.a,g=e.a,h=d.b.a,k=e.b.a,l=new N;x(h,a.a,f);x(k,a.a,g);if(f===g||Math.min(f.a,h.a)>Math.max(g.a,k.a))return!1;if(u(f,g)){if(0x(h,g,f))return!1;var r=h,p=f,q=k,y=g,m,w;u(r,p)||(m=r,r=p,p=m);u(q,y)||(m=q,q=y,y=m);u(r,q)||(m=r,r=q,q=m,m=p,p=y,y=m);u(q,p)?u(p,y)?(m=v(r,q,p),w=v(q,p,y),0>m+w&&(m=-m,w=-w),l.b=A(m,q.b,w,p.b)):(m=x(r,q,p),w=-x(r,y,p),0>m+w&&(m=-m,w=-w),l.b=A(m,q.b,w,y.b)):l.b=(q.b+p.b)/2;z(r,p)||(m=r,r=p,p=m);z(q,y)|| 39 | (m=q,q=y,y=m);z(r,q)||(m=r,r=q,q=m,m=p,p=y,y=m);z(q,p)?z(p,y)?(m=aa(r,q,p),w=aa(q,p,y),0>m+w&&(m=-m,w=-w),l.a=A(m,q.a,w,p.a)):(m=ba(r,q,p),w=-ba(r,y,p),0>m+w&&(m=-m,w=-w),l.a=A(m,q.a,w,y.a)):l.a=(q.a+p.a)/2;u(l,a.a)&&(l.b=a.a.b,l.a=a.a.a);r=u(f,g)?f:g;u(r,l)&&(l.b=r.b,l.a=r.a);if(t(l,f)||t(l,g))return qa(a,b),!1;if(!t(h,a.a)&&0<=x(h,a.a,l)||!t(k,a.a)&&0>=x(k,a.a,l)){if(k===a.a)return K(d.b),E(e.b,d),b=ka(b),d=S(b).a,T(a,S(b),c),U(a,b,J(d),d,d,!0),!0;if(h===a.a){K(e.b);E(d.e,J(e));f=c=b;g=f.a.b.a; 40 | do f=R(f);while(f.a.b.a===g);b=f;f=S(b).a.b.c;c.a=J(e);e=T(a,c,null);U(a,b,e.c,d.b.c,f,!0);return!0}0<=x(h,a.a,l)&&(R(b).b=b.b=!0,K(d.b),d.a.b=a.a.b,d.a.a=a.a.a);0>=x(k,a.a,l)&&(b.b=c.b=!0,K(e.b),e.a.b=a.a.b,e.a.a=a.a.a);return!1}K(d.b);K(e.b);E(J(e),d);d.a.b=l.b;d.a.a=l.a;d.a.h=xa(a.e,d.a);d=d.a;e=[0,0,0,0];l=[f.d,h.d,g.d,k.d];d.g[0]=d.g[1]=d.g[2]=0;ua(d,f,h,e,0);ua(d,g,k,e,2);sa(a,d,l,e,!0);R(b).b=b.b=c.b=!0;return!1} 41 | function ra(a,b){for(var c=S(b);;){for(;c.b;)b=c,c=S(c);if(!b.b&&(c=b,b=R(b),null===b||!b.b))break;b.b=!1;var d=b.a,e=c.a,f;if(f=d.b.a!==e.b.a)a:{f=b;var g=S(f),h=f.a,k=g.a,l=void 0;if(u(h.b.a,k.b.a)){if(0>x(h.b.a,k.b.a,h.a)){f=!1;break a}R(f).b=f.b=!0;l=K(h);E(k.b,l);l.d.c=f.d}else{if(0e;++e){var f=a[e];-1E150>f&&(f=-1E150,c=!0);1E150h;++h){var k=a.g[h];kb[h]&&(b[h]=k,c[h]=a)}a=0;b[1]-f[1]>b[0]-f[0]&&(a=1);b[2]-f[2]>b[a]-f[a]&&(a=2);if(f[a]>=b[a])e[0]=0,e[1]=0,e[2]=1;else{b=0;f=g[a];c=c[a];g=[0,0,0];f=[f.g[0]-c.g[0],f.g[1]-c.g[1],f.g[2]-c.g[2]];h=[0,0,0];for(a=d.e;a!==d;a= 49 | a.e)h[0]=a.g[0]-c.g[0],h[1]=a.g[1]-c.g[1],h[2]=a.g[2]-c.g[2],g[0]=f[1]*h[2]-f[2]*h[1],g[1]=f[2]*h[0]-f[0]*h[2],g[2]=f[0]*h[1]-f[1]*h[0],k=g[0]*g[0]+g[1]*g[1]+g[2]*g[2],k>b&&(b=k,e[0]=g[0],e[1]=g[1],e[2]=g[2]);0>=b&&(e[0]=e[1]=e[2]=0,e[ha(f)]=1)}d=!0}g=ha(e);a=this.b.c;b=(g+1)%3;c=(g+2)%3;g=0=b.f)){do e+=(b.a.b-b.b.a.b)*(b.a.a+b.b.a.a),b=b.e;while(b!==a.a)}if(0>e)for(e=this.b.c,d=e.e;d!== 50 | e;d=d.e)d.a=-d.a}this.n=!1;e=this.b.b;for(a=e.h;a!==e;a=d)if(d=a.h,b=a.e,t(a.a,a.b.a)&&a.e.e!==a&&(ta(this,b,a),I(a),a=b,b=a.e),b.e===a){if(b!==a){if(b===d||b===d.b)d=d.h;I(b)}if(a===d||a===d.b)d=d.h;I(a)}this.e=e=new Da;d=this.b.c;for(a=d.e;a!==d;a=a.e)a.h=xa(e,a);Ea(e);this.f=new Aa(this);za(this,-O);for(za(this,O);null!==(e=Fa(this.e));){for(;;){a:if(a=this.e,0===a.a)d=Ga(a.b);else if(d=a.c[a.d[a.a-1]],0!==a.b.a&&(a=Ga(a.b),u(a,d))){d=a;break a}if(null===d||!t(d,e))break;d=Fa(this.e);ta(this,e.c, 51 | d.c)}ya(this,e)}this.a=this.f.a.a.b.a.a;for(e=0;null!==(d=this.f.a.a.b);)d.h||++e,Q(d);this.f=null;e=this.e;e.b=null;e.d=null;this.e=e.c=null;e=this.b;for(a=e.a.b;a!==e.a;a=d)d=a.b,a=a.a,a.e.e===a&&(P(a.c,a),I(a));if(!this.n){e=this.b;if(this.m)for(a=e.b.h;a!==e.b;a=d)d=a.h,a.b.d.c!==a.d.c?a.f=a.d.c?1:-1:I(a);else for(a=e.a.b;a!==e.a;a=d)if(d=a.b,a.c){for(a=a.a;u(a.b.a,a.a);a=a.c.b);for(;u(a.a,a.b.a);a=a.e);b=a.c.b;for(c=void 0;a.e!==b;)if(u(a.b.a,b.a)){for(;b.e!==a&&(ca(b.e)||0>=x(b.a,b.b.a,b.e.b.a));)c= 52 | L(b.e,b),b=c.b;b=b.c.b}else{for(;b.e!==a&&(da(a.c.b)||0<=x(a.b.a,a.a,a.c.b.a));)c=L(a,a.c.b),a=c.b;a=a.e}for(;b.e.e!==a;)c=L(b.e,b),b=c.b}if(this.h||this.i||this.k||this.l)if(this.m)for(e=this.b,d=e.a.b;d!==e.a;d=d.b){if(d.c){this.h&&this.h(2,this.c);a=d.a;do this.k&&this.k(a.a.d,this.c),a=a.e;while(a!==d.a);this.i&&this.i(this.c)}}else{e=this.b;d=!!this.l;a=!1;b=-1;for(c=e.a.d;c!==e.a;c=c.d)if(c.c){a||(this.h&&this.h(4,this.c),a=!0);g=c.a;do d&&(f=g.b.d.c?0:1,b!==f&&(b=f,this.l&&this.l(!!b,this.c))), 53 | this.k&&this.k(g.a.d,this.c),g=g.e;while(g!==c.a)}a&&this.i&&this.i(this.c)}if(this.r){e=this.b;for(a=e.a.b;a!==e.a;a=d)if(d=a.b,!a.c){b=a.a;c=b.e;g=void 0;do g=c,c=g.e,g.d=null,null===g.b.d&&(g.c===g?F(g.a,null):(g.a.c=g.c,H(g,J(g))),f=g.b,f.c===f?F(f.a,null):(f.a.c=f.c,H(f,J(f))),fa(g));while(g!==b);b=a.d;a=a.b;a.d=b;b.b=a}this.r(this.b);this.c=this.b=null;return}}this.b=this.c=null}; 54 | function Z(a,b){if(a.d!==b)for(;a.d!==b;)if(a.dc.f&&(c.f*=2,c.c=Ja(c.c,c.f+1));var e;0===c.b?e=d:(e=c.b,c.b=c.c[c.b]);c.e[e]=b;c.c[e]=d;c.d[d]=e;c.h&&va(c,d);return e}c=a.a++;a.c[c]=b;return-(c+1)} 55 | function Fa(a){if(0===a.a)return Ka(a.b);var b=a.c[a.d[a.a-1]];if(0!==a.b.a&&u(Ga(a.b),b))return Ka(a.b);do--a.a;while(0a.a||u(d[g],d[k])){c[f]=g;e[g]=f;break}c[f]=k;e[k]=f;f=h}}function va(a,b){for(var c=a.d,d=a.e,e=a.c,f=b,g=c[f];;){var h=f>>1,k=c[h];if(0===h||u(d[k],d[g])){c[f]=g;e[g]=f;break}c[f]=k;e[k]=f;f=h}};function ma(){this.e=this.a=null;this.f=0;this.c=this.b=this.h=this.d=!1}function S(a){return a.e.c.b}function R(a){return a.e.a.b};this.libtess={GluTesselator:X,windingRule:{GLU_TESS_WINDING_ODD:100130,GLU_TESS_WINDING_NONZERO:100131,GLU_TESS_WINDING_POSITIVE:100132,GLU_TESS_WINDING_NEGATIVE:100133,GLU_TESS_WINDING_ABS_GEQ_TWO:100134},primitiveType:{GL_LINE_LOOP:2,GL_TRIANGLES:4,GL_TRIANGLE_STRIP:5,GL_TRIANGLE_FAN:6},errorType:{GLU_TESS_MISSING_BEGIN_POLYGON:100151,GLU_TESS_MISSING_END_POLYGON:100153,GLU_TESS_MISSING_BEGIN_CONTOUR:100152,GLU_TESS_MISSING_END_CONTOUR:100154,GLU_TESS_COORD_TOO_LARGE:100155,GLU_TESS_NEED_COMBINE_CALLBACK:100156}, 57 | gluEnum:{GLU_TESS_MESH:100112,GLU_TESS_TOLERANCE:100142,GLU_TESS_WINDING_RULE:100140,GLU_TESS_BOUNDARY_ONLY:100141,GLU_INVALID_ENUM:100900,GLU_INVALID_VALUE:100901,GLU_TESS_BEGIN:100100,GLU_TESS_VERTEX:100101,GLU_TESS_END:100102,GLU_TESS_ERROR:100103,GLU_TESS_EDGE_FLAG:100104,GLU_TESS_COMBINE:100105,GLU_TESS_BEGIN_DATA:100106,GLU_TESS_VERTEX_DATA:100107,GLU_TESS_END_DATA:100108,GLU_TESS_ERROR_DATA:100109,GLU_TESS_EDGE_FLAG_DATA:100110,GLU_TESS_COMBINE_DATA:100111}};X.prototype.gluDeleteTess=X.prototype.x; 58 | X.prototype.gluTessProperty=X.prototype.B;X.prototype.gluGetTessProperty=X.prototype.y;X.prototype.gluTessNormal=X.prototype.A;X.prototype.gluTessCallback=X.prototype.z;X.prototype.gluTessVertex=X.prototype.C;X.prototype.gluTessBeginPolygon=X.prototype.u;X.prototype.gluTessBeginContour=X.prototype.t;X.prototype.gluTessEndContour=X.prototype.v;X.prototype.gluTessEndPolygon=X.prototype.w; if (typeof module !== 'undefined') { module.exports = this.libtess; } 59 | -------------------------------------------------------------------------------- /src/public/assets/js/rStats.extras.js: -------------------------------------------------------------------------------- 1 | window.glStats = function () { 2 | 3 | var _rS = null; 4 | 5 | var _totalDrawArraysCalls = 0, 6 | _totalDrawElementsCalls = 0, 7 | _totalUseProgramCalls = 0, 8 | _totalFaces = 0, 9 | _totalVertices = 0, 10 | _totalPoints = 0, 11 | _totalBindTexures = 0; 12 | 13 | function _h ( f, c ) { 14 | return function () { 15 | c.apply( this, arguments ); 16 | f.apply( this, arguments ); 17 | }; 18 | } 19 | 20 | WebGLRenderingContext.prototype.drawArrays = _h( WebGLRenderingContext.prototype.drawArrays, function () { 21 | _totalDrawArraysCalls++; 22 | if ( arguments[ 0 ] == this.POINTS ) _totalPoints += arguments[ 2 ]; 23 | else _totalVertices += arguments[ 2 ]; 24 | } ); 25 | 26 | WebGLRenderingContext.prototype.drawElements = _h( WebGLRenderingContext.prototype.drawElements, function () { 27 | _totalDrawElementsCalls++; 28 | _totalFaces += arguments[ 1 ] / 3; 29 | _totalVertices += arguments[ 1 ]; 30 | } ); 31 | 32 | WebGLRenderingContext.prototype.useProgram = _h( WebGLRenderingContext.prototype.useProgram, function () { 33 | _totalUseProgramCalls++; 34 | } ); 35 | 36 | WebGLRenderingContext.prototype.bindTexture = _h( WebGLRenderingContext.prototype.bindTexture, function () { 37 | _totalBindTexures++; 38 | } ); 39 | 40 | var _values = { 41 | allcalls: { 42 | over: 3000, 43 | caption: 'Calls (hook)' 44 | }, 45 | drawelements: { 46 | caption: 'drawElements (hook)' 47 | }, 48 | drawarrays: { 49 | caption: 'drawArrays (hook)' 50 | } 51 | }; 52 | 53 | var _groups = [ { 54 | caption: 'WebGL', 55 | values: [ 'allcalls', 'drawelements', 'drawarrays', 'useprogram', 'bindtexture', 'glfaces', 'glvertices', 'glpoints' ] 56 | } ]; 57 | 58 | var _fractions = [ { 59 | base: 'allcalls', 60 | steps: [ 'drawelements', 'drawarrays' ] 61 | } ]; 62 | 63 | function _update () { 64 | _rS( 'allcalls' ).set( _totalDrawArraysCalls + _totalDrawElementsCalls ); 65 | _rS( 'drawElements' ).set( _totalDrawElementsCalls ); 66 | _rS( 'drawArrays' ).set( _totalDrawArraysCalls ); 67 | _rS( 'bindTexture' ).set( _totalBindTexures ); 68 | _rS( 'useProgram' ).set( _totalUseProgramCalls ); 69 | _rS( 'glfaces' ).set( _totalFaces ); 70 | _rS( 'glvertices' ).set( _totalVertices ); 71 | _rS( 'glpoints' ).set( _totalPoints ); 72 | } 73 | 74 | function _start () { 75 | _totalDrawArraysCalls = 0; 76 | _totalDrawElementsCalls = 0; 77 | _totalUseProgramCalls = 0; 78 | _totalFaces = 0; 79 | _totalVertices = 0; 80 | _totalPoints = 0; 81 | _totalBindTexures = 0; 82 | } 83 | 84 | function _end () {} 85 | 86 | function _attach ( r ) { 87 | _rS = r; 88 | } 89 | 90 | return { 91 | update: _update, 92 | start: _start, 93 | end: _end, 94 | attach: _attach, 95 | values: _values, 96 | groups: _groups, 97 | fractions: _fractions 98 | }; 99 | 100 | }; 101 | 102 | window.threeStats = function ( renderer ) { 103 | 104 | var _rS = null; 105 | 106 | var _values = { 107 | 'renderer.info.memory.geometries': { 108 | caption: 'Geometries' 109 | }, 110 | 'renderer.info.memory.textures': { 111 | caption: 'Textures' 112 | }, 113 | 'renderer.info.programs': { 114 | caption: 'Programs' 115 | }, 116 | 'renderer.info.render.calls': { 117 | caption: 'Calls' 118 | }, 119 | 'renderer.info.render.faces': { 120 | caption: 'Faces', 121 | over: 1000 122 | }, 123 | 'renderer.info.render.points': { 124 | caption: 'Points' 125 | }, 126 | 'renderer.info.render.vertices': { 127 | caption: 'Vertices' 128 | } 129 | }; 130 | 131 | var _groups = [ { 132 | caption: 'Three.js - Memory', 133 | values: [ 'renderer.info.memory.geometries', 'renderer.info.programs', 'renderer.info.memory.textures' ] 134 | }, { 135 | caption: 'Three.js - Render', 136 | values: [ 'renderer.info.render.calls', 'renderer.info.render.faces', 'renderer.info.render.points', 'renderer.info.render.vertices' ] 137 | } ]; 138 | 139 | var _fractions = []; 140 | 141 | function _update () { 142 | 143 | _rS( 'renderer.info.memory.geometries' ).set( renderer.info.memory.geometries ); 144 | //_rS( 'renderer.info.programs' ).set( renderer.info.programs.length ); 145 | _rS( 'renderer.info.memory.textures' ).set( renderer.info.memory.textures ); 146 | _rS( 'renderer.info.render.calls' ).set( renderer.info.render.calls ); 147 | _rS( 'renderer.info.render.faces' ).set( renderer.info.render.faces ); 148 | _rS( 'renderer.info.render.points' ).set( renderer.info.render.points ); 149 | _rS( 'renderer.info.render.vertices' ).set( renderer.info.render.vertices ); 150 | 151 | } 152 | 153 | function _start () {} 154 | 155 | function _end () {} 156 | 157 | function _attach ( r ) { 158 | _rS = r; 159 | } 160 | 161 | return { 162 | update: _update, 163 | start: _start, 164 | end: _end, 165 | attach: _attach, 166 | values: _values, 167 | groups: _groups, 168 | fractions: _fractions 169 | }; 170 | 171 | }; 172 | 173 | /* 174 | * From https://github.com/paulirish/memory-stats.js 175 | */ 176 | 177 | window.BrowserStats = function () { 178 | 179 | var _rS = null; 180 | 181 | var _usedJSHeapSize = 0, 182 | _totalJSHeapSize = 0; 183 | 184 | var memory = { 185 | usedJSHeapSize: 0, 186 | totalJSHeapSize: 0 187 | }; 188 | 189 | if ( window.performance && performance.memory ) 190 | memory = performance.memory; 191 | 192 | if ( memory.totalJSHeapSize === 0 ) { 193 | console.warn( 'totalJSHeapSize === 0... performance.memory is only available in Chrome .' ); 194 | } 195 | 196 | var _values = { 197 | memory: { 198 | caption: 'Used Memory', 199 | average: true, 200 | avgMs: 1000, 201 | over: 22 202 | }, 203 | total: { 204 | caption: 'Total Memory' 205 | } 206 | }; 207 | 208 | var _groups = [ { 209 | caption: 'Browser', 210 | values: [ 'memory', 'total' ] 211 | } ]; 212 | 213 | var _fractions = [ { 214 | base: 'total', 215 | steps: [ 'memory' ] 216 | } ]; 217 | 218 | var log1024 = Math.log( 1024 ); 219 | 220 | function _size ( v ) { 221 | 222 | var precision = 100; //Math.pow(10, 2); 223 | var i = Math.floor( Math.log( v ) / log1024 ); 224 | if( v === 0 ) i = 1; 225 | return Math.round( v * precision / Math.pow( 1024, i ) ) / precision; // + ' ' + sizes[i]; 226 | 227 | } 228 | 229 | function _update () { 230 | _usedJSHeapSize = _size( memory.usedJSHeapSize ); 231 | _totalJSHeapSize = _size( memory.totalJSHeapSize ); 232 | 233 | _rS( 'memory' ).set( _usedJSHeapSize ); 234 | _rS( 'total' ).set( _totalJSHeapSize ); 235 | } 236 | 237 | function _start () { 238 | _usedJSHeapSize = 0; 239 | } 240 | 241 | function _end () {} 242 | 243 | function _attach ( r ) { 244 | _rS = r; 245 | } 246 | 247 | return { 248 | update: _update, 249 | start: _start, 250 | end: _end, 251 | attach: _attach, 252 | values: _values, 253 | groups: _groups, 254 | fractions: _fractions 255 | }; 256 | 257 | }; 258 | 259 | if (typeof module === 'object') { 260 | module.exports = { 261 | glStats: window.glStats, 262 | threeStats: window.threeStats, 263 | BrowserStats: window.BrowserStats 264 | }; 265 | } 266 | -------------------------------------------------------------------------------- /src/public/assets/js/rStats.js: -------------------------------------------------------------------------------- 1 | // performance.now() polyfill from https://gist.github.com/paulirish/5438650 2 | 'use strict'; 3 | 4 | ( function () { 5 | 6 | // prepare base perf object 7 | if ( typeof window.performance === 'undefined' ) { 8 | window.performance = {}; 9 | } 10 | 11 | if ( !window.performance.now ) { 12 | 13 | var nowOffset = Date.now(); 14 | 15 | if ( performance.timing && performance.timing.navigationStart ) { 16 | nowOffset = performance.timing.navigationStart; 17 | } 18 | 19 | window.performance.now = function now () { 20 | return Date.now() - nowOffset; 21 | }; 22 | 23 | } 24 | 25 | if( !window.performance.mark ) { 26 | window.performance.mark = function(){} 27 | } 28 | 29 | if( !window.performance.measure ) { 30 | window.performance.measure = function(){} 31 | } 32 | 33 | } )(); 34 | 35 | window.rStats = function rStats ( settings ) { 36 | 37 | function iterateKeys ( array, callback ) { 38 | var keys = Object.keys( array ); 39 | for ( var j = 0, l = keys.length; j < l; j++ ) { 40 | callback( keys[ j ] ); 41 | } 42 | } 43 | 44 | function importCSS ( url ) { 45 | 46 | var element = document.createElement( 'link' ); 47 | element.href = url; 48 | element.rel = 'stylesheet'; 49 | element.type = 'text/css'; 50 | document.getElementsByTagName( 'head' )[ 0 ].appendChild( element ); 51 | 52 | } 53 | 54 | var _settings = settings || {}; 55 | var _colours = _settings.colours || [ '#850700', '#c74900', '#fcb300', '#284280', '#4c7c0c' ]; 56 | 57 | var _cssFont = 'https://fonts.googleapis.com/css?family=Roboto+Condensed:400,700,300'; 58 | var _cssRStats = ( _settings.CSSPath ? _settings.CSSPath : '' ) + 'rStats.css'; 59 | 60 | var _css = _settings.css || [ _cssFont, _cssRStats ]; 61 | _css.forEach(function (uri) { 62 | importCSS( uri ); 63 | }); 64 | 65 | if ( !_settings.values ) _settings.values = {}; 66 | 67 | var _base, _div, _elHeight = 10, _elWidth = 200; 68 | var _perfCounters = {}; 69 | 70 | 71 | function Graph ( _dom, _id, _defArg ) { 72 | 73 | var _def = _defArg || {}; 74 | var _canvas = document.createElement( 'canvas' ), 75 | _ctx = _canvas.getContext( '2d' ), 76 | _max = 0, 77 | _current = 0; 78 | 79 | var c = _def.color ? _def.color : '#666666'; 80 | 81 | var _dotCanvas = document.createElement( 'canvas' ), 82 | _dotCtx = _dotCanvas.getContext( '2d' ); 83 | _dotCanvas.width = 1; 84 | _dotCanvas.height = 2 * _elHeight; 85 | _dotCtx.fillStyle = '#444444'; 86 | _dotCtx.fillRect( 0, 0, 1, 2 * _elHeight ); 87 | _dotCtx.fillStyle = c; 88 | _dotCtx.fillRect( 0, _elHeight, 1, _elHeight ); 89 | _dotCtx.fillStyle = '#ffffff'; 90 | _dotCtx.globalAlpha = 0.5; 91 | _dotCtx.fillRect( 0, _elHeight, 1, 1 ); 92 | _dotCtx.globalAlpha = 1; 93 | 94 | var _alarmCanvas = document.createElement( 'canvas' ), 95 | _alarmCtx = _alarmCanvas.getContext( '2d' ); 96 | _alarmCanvas.width = 1; 97 | _alarmCanvas.height = 2 * _elHeight; 98 | _alarmCtx.fillStyle = '#444444'; 99 | _alarmCtx.fillRect( 0, 0, 1, 2 * _elHeight ); 100 | _alarmCtx.fillStyle = '#b70000'; 101 | _alarmCtx.fillRect( 0, _elHeight, 1, _elHeight ); 102 | _alarmCtx.globalAlpha = 0.5; 103 | _alarmCtx.fillStyle = '#ffffff'; 104 | _alarmCtx.fillRect( 0, _elHeight, 1, 1 ); 105 | _alarmCtx.globalAlpha = 1; 106 | 107 | function _init () { 108 | 109 | _canvas.width = _elWidth; 110 | _canvas.height = _elHeight; 111 | _canvas.style.width = _canvas.width + 'px'; 112 | _canvas.style.height = _canvas.height + 'px'; 113 | _canvas.className = 'rs-canvas'; 114 | _dom.appendChild( _canvas ); 115 | 116 | _ctx.fillStyle = '#444444'; 117 | _ctx.fillRect( 0, 0, _canvas.width, _canvas.height ); 118 | 119 | } 120 | 121 | function _draw ( v, alarm ) { 122 | _current += ( v - _current ) * 0.1; 123 | _max *= 0.99; 124 | if ( _current > _max ) _max = _current; 125 | _ctx.drawImage( _canvas, 1, 0, _canvas.width - 1, _canvas.height, 0, 0, _canvas.width - 1, _canvas.height ); 126 | if ( alarm ) { 127 | _ctx.drawImage( _alarmCanvas, _canvas.width - 1, _canvas.height - _current * _canvas.height / _max - _elHeight ); 128 | } else { 129 | _ctx.drawImage( _dotCanvas, _canvas.width - 1, _canvas.height - _current * _canvas.height / _max - _elHeight ); 130 | } 131 | } 132 | 133 | _init(); 134 | 135 | return { 136 | draw: _draw 137 | }; 138 | 139 | } 140 | 141 | function StackGraph ( _dom, _num ) { 142 | 143 | var _canvas = document.createElement( 'canvas' ), 144 | _ctx = _canvas.getContext( '2d' ); 145 | 146 | function _init () { 147 | 148 | _canvas.width = _elWidth; 149 | _canvas.height = _elHeight * _num; 150 | _canvas.style.width = _canvas.width + 'px'; 151 | _canvas.style.height = _canvas.height + 'px'; 152 | _canvas.className = 'rs-canvas'; 153 | _dom.appendChild( _canvas ); 154 | 155 | _ctx.fillStyle = '#444444'; 156 | _ctx.fillRect( 0, 0, _canvas.width, _canvas.height ); 157 | 158 | } 159 | 160 | function _draw ( v ) { 161 | _ctx.drawImage( _canvas, 1, 0, _canvas.width - 1, _canvas.height, 0, 0, _canvas.width - 1, _canvas.height ); 162 | var th = 0; 163 | iterateKeys( v, function ( j ) { 164 | var h = v[ j ] * _canvas.height; 165 | _ctx.fillStyle = _colours[ j ]; 166 | _ctx.fillRect( _canvas.width - 1, th, 1, h ); 167 | th += h; 168 | } ); 169 | } 170 | 171 | _init(); 172 | 173 | return { 174 | draw: _draw 175 | }; 176 | 177 | } 178 | 179 | function PerfCounter ( id, group ) { 180 | 181 | var _id = id, 182 | _time, 183 | _value = 0, 184 | _total = 0, 185 | _averageValue = 0, 186 | _accumValue = 0, 187 | _accumStart = performance.now(), 188 | _accumSamples = 0, 189 | _dom = document.createElement( 'div' ), 190 | _spanId = document.createElement( 'span' ), 191 | _spanValue = document.createElement( 'div' ), 192 | _spanValueText = document.createTextNode( '' ), 193 | _def = _settings ? _settings.values[ _id.toLowerCase() ] : null, 194 | _graph = new Graph( _dom, _id, _def ), 195 | _started = false; 196 | 197 | _dom.className = 'rs-counter-base'; 198 | 199 | _spanId.className = 'rs-counter-id'; 200 | _spanId.textContent = ( _def && _def.caption ) ? _def.caption : _id; 201 | 202 | _spanValue.className = 'rs-counter-value'; 203 | _spanValue.appendChild( _spanValueText ); 204 | 205 | _dom.appendChild( _spanId ); 206 | _dom.appendChild( _spanValue ); 207 | if ( group ) group.div.appendChild( _dom ); 208 | else _div.appendChild( _dom ); 209 | 210 | _time = performance.now(); 211 | 212 | function _average ( v ) { 213 | if ( _def && _def.average ) { 214 | _accumValue += v; 215 | _accumSamples++; 216 | var t = performance.now(); 217 | if ( t - _accumStart >= ( _def.avgMs || 1000 ) ) { 218 | _averageValue = _accumValue / _accumSamples; 219 | _accumValue = 0; 220 | _accumStart = t; 221 | _accumSamples = 0; 222 | } 223 | } 224 | } 225 | 226 | function _start () { 227 | _time = performance.now(); 228 | if( _settings.userTimingAPI ) performance.mark( _id + '-start' ); 229 | _started = true; 230 | } 231 | 232 | function _end () { 233 | _value = performance.now() - _time; 234 | if( _settings.userTimingAPI ) { 235 | performance.mark( _id + '-end' ); 236 | if( _started ) { 237 | performance.measure( _id, _id + '-start', _id + '-end' ); 238 | } 239 | } 240 | _average( _value ); 241 | } 242 | 243 | function _tick () { 244 | _end(); 245 | _start(); 246 | } 247 | 248 | function _draw () { 249 | var v = ( _def && _def.average ) ? _averageValue : _value; 250 | _spanValueText.nodeValue = Math.round( v * 100 ) / 100; 251 | var a = ( _def && ( ( _def.below && _value < _def.below ) || ( _def.over && _value > _def.over ) ) ); 252 | _graph.draw( _value, a ); 253 | _dom.style.color = a ? '#b70000' : '#ffffff'; 254 | } 255 | 256 | function _frame () { 257 | var t = performance.now(); 258 | var e = t - _time; 259 | _total++; 260 | if ( e > 1000 ) { 261 | if ( _def && _def.interpolate === false ) { 262 | _value = _total; 263 | } else { 264 | _value = _total * 1000 / e; 265 | } 266 | _total = 0; 267 | _time = t; 268 | _average( _value ); 269 | } 270 | } 271 | 272 | function _set ( v ) { 273 | _value = v; 274 | _average( _value ); 275 | } 276 | 277 | return { 278 | set: _set, 279 | start: _start, 280 | tick: _tick, 281 | end: _end, 282 | frame: _frame, 283 | value: function () { 284 | return _value; 285 | }, 286 | draw: _draw 287 | }; 288 | 289 | } 290 | 291 | function sample () { 292 | 293 | var _value = 0; 294 | 295 | function _set ( v ) { 296 | _value = v; 297 | } 298 | 299 | return { 300 | set: _set, 301 | value: function () { 302 | return _value; 303 | } 304 | }; 305 | 306 | } 307 | 308 | function _perf ( idArg ) { 309 | 310 | var id = idArg.toLowerCase(); 311 | if ( id === undefined ) id = 'default'; 312 | if ( _perfCounters[ id ] ) return _perfCounters[ id ]; 313 | 314 | var group = null; 315 | if ( _settings && _settings.groups ) { 316 | iterateKeys( _settings.groups, function ( j ) { 317 | var g = _settings.groups[ parseInt( j, 10 ) ]; 318 | if ( !group && g.values.indexOf( id.toLowerCase() ) !== -1 ) { 319 | group = g; 320 | } 321 | } ); 322 | } 323 | 324 | var p = new PerfCounter( id, group ); 325 | _perfCounters[ id ] = p; 326 | return p; 327 | 328 | } 329 | 330 | function _init () { 331 | 332 | if ( _settings.plugins ) { 333 | if ( !_settings.values ) _settings.values = {}; 334 | if ( !_settings.groups ) _settings.groups = []; 335 | if ( !_settings.fractions ) _settings.fractions = []; 336 | for ( var j = 0; j < _settings.plugins.length; j++ ) { 337 | _settings.plugins[ j ].attach( _perf ); 338 | iterateKeys( _settings.plugins[ j ].values, function ( k ) { 339 | _settings.values[ k ] = _settings.plugins[ j ].values[ k ]; 340 | } ); 341 | _settings.groups = _settings.groups.concat( _settings.plugins[ j ].groups ); 342 | _settings.fractions = _settings.fractions.concat( _settings.plugins[ j ].fractions ); 343 | } 344 | } else { 345 | _settings.plugins = {}; 346 | } 347 | 348 | _base = document.createElement( 'div' ); 349 | _base.className = 'rs-base'; 350 | _div = document.createElement( 'div' ); 351 | _div.className = 'rs-container'; 352 | _div.style.height = 'auto'; 353 | _base.appendChild( _div ); 354 | document.body.appendChild( _base ); 355 | 356 | if ( !_settings ) return; 357 | 358 | if ( _settings.groups ) { 359 | iterateKeys( _settings.groups, function ( j ) { 360 | var g = _settings.groups[ parseInt( j, 10 ) ]; 361 | var div = document.createElement( 'div' ); 362 | div.className = 'rs-group'; 363 | g.div = div; 364 | var h1 = document.createElement( 'h1' ); 365 | h1.textContent = g.caption; 366 | h1.addEventListener( 'click', function ( e ) { 367 | this.classList.toggle( 'hidden' ); 368 | e.preventDefault(); 369 | }.bind( div ) ); 370 | _div.appendChild( h1 ); 371 | _div.appendChild( div ); 372 | } ); 373 | } 374 | 375 | if ( _settings.fractions ) { 376 | iterateKeys( _settings.fractions, function ( j ) { 377 | var f = _settings.fractions[ parseInt( j, 10 ) ]; 378 | var div = document.createElement( 'div' ); 379 | div.className = 'rs-fraction'; 380 | var legend = document.createElement( 'div' ); 381 | legend.className = 'rs-legend'; 382 | 383 | var h = 0; 384 | iterateKeys( _settings.fractions[ j ].steps, function ( k ) { 385 | var p = document.createElement( 'p' ); 386 | p.textContent = _settings.fractions[ j ].steps[ k ]; 387 | p.style.color = _colours[ h ]; 388 | legend.appendChild( p ); 389 | h++; 390 | } ); 391 | div.appendChild( legend ); 392 | div.style.height = h * _elHeight + 'px'; 393 | f.div = div; 394 | var graph = new StackGraph( div, h ); 395 | f.graph = graph; 396 | _div.appendChild( div ); 397 | } ); 398 | } 399 | 400 | } 401 | 402 | function _update () { 403 | 404 | iterateKeys( _settings.plugins, function ( j ) { 405 | _settings.plugins[ j ].update(); 406 | } ); 407 | 408 | iterateKeys( _perfCounters, function ( j ) { 409 | _perfCounters[ j ].draw(); 410 | } ); 411 | 412 | if ( _settings && _settings.fractions ) { 413 | iterateKeys( _settings.fractions, function ( j ) { 414 | var f = _settings.fractions[ parseInt( j, 10 ) ]; 415 | var v = []; 416 | var base = _perfCounters[ f.base.toLowerCase() ]; 417 | if ( base ) { 418 | base = base.value(); 419 | iterateKeys( _settings.fractions[ j ].steps, function ( k ) { 420 | var s = _settings.fractions[ j ].steps[ parseInt( k, 10 ) ].toLowerCase(); 421 | var val = _perfCounters[ s ]; 422 | if ( val ) { 423 | v.push( val.value() / base ); 424 | } 425 | } ); 426 | } 427 | f.graph.draw( v ); 428 | } ); 429 | } 430 | 431 | /*if( _height != _div.clientHeight ) { 432 | _height = _div.clientHeight; 433 | _base.style.height = _height + 2 * _elHeight + 'px'; 434 | console.log( _base.clientHeight ); 435 | }*/ 436 | 437 | } 438 | 439 | _init(); 440 | 441 | return function ( id ) { 442 | if ( id ) return _perf( id ); 443 | return { 444 | element: _base, 445 | update: _update 446 | }; 447 | }; 448 | 449 | } 450 | 451 | if (typeof module === 'object') { 452 | module.exports = window.rStats; 453 | } 454 | -------------------------------------------------------------------------------- /src/public/assets/js/simplify3D.js: -------------------------------------------------------------------------------- 1 | /* 2 | (c) 2013, Vladimir Agafonkin 3 | Simplify.js, a high-performance JS polyline simplification library 4 | mourner.github.io/simplify-js 5 | */ 6 | 7 | (function () { "use strict"; 8 | 9 | // to suit your point format, run search/replace for '.x', '.y' and '.z'; 10 | // (configurability would draw significant performance overhead) 11 | 12 | // square distance between 2 points 13 | function getSquareDistance(p1, p2) { 14 | 15 | var dx = p1.x - p2.x, 16 | dy = p1.y - p2.y, 17 | dz = p1.z - p2.z; 18 | 19 | return dx * dx + dy * dy + dz * dz; 20 | } 21 | 22 | // square distance from a point to a segment 23 | function getSquareSegmentDistance(p, p1, p2) { 24 | 25 | var x = p1.x, 26 | y = p1.y, 27 | z = p1.z, 28 | 29 | dx = p2.x - x, 30 | dy = p2.y - y, 31 | dz = p2.z - z; 32 | 33 | if (dx !== 0 || dy !== 0 || dz !== 0) { 34 | 35 | var t = ((p.x - x) * dx + (p.y - y) * dy + (p.z - z) * dz) / 36 | (dx * dx + dy * dy + dz * dz); 37 | 38 | if (t > 1) { 39 | x = p2.x; 40 | y = p2.y; 41 | z = p2.z; 42 | 43 | } else if (t > 0) { 44 | x += dx * t; 45 | y += dy * t; 46 | z += dz * t; 47 | } 48 | } 49 | 50 | dx = p.x - x; 51 | dy = p.y - y; 52 | dz = p.z - z; 53 | 54 | return dx * dx + dy * dy + dz * dz; 55 | } 56 | // the rest of the code doesn't care for the point format 57 | 58 | // basic distance-based simplification 59 | function simplifyRadialDistance(points, sqTolerance) { 60 | 61 | var prevPoint = points[0], 62 | newPoints = [prevPoint], 63 | point; 64 | 65 | for (var i = 1, len = points.length; i < len; i++) { 66 | point = points[i]; 67 | 68 | if (getSquareDistance(point, prevPoint) > sqTolerance) { 69 | newPoints.push(point); 70 | prevPoint = point; 71 | } 72 | } 73 | 74 | if (prevPoint !== point) { 75 | newPoints.push(point); 76 | } 77 | 78 | return newPoints; 79 | } 80 | 81 | // simplification using optimized Douglas-Peucker algorithm with recursion elimination 82 | function simplifyDouglasPeucker(points, sqTolerance) { 83 | 84 | var len = points.length, 85 | MarkerArray = typeof Uint8Array !== 'undefined' ? Uint8Array : Array, 86 | markers = new MarkerArray(len), 87 | 88 | first = 0, 89 | last = len - 1, 90 | 91 | stack = [], 92 | newPoints = [], 93 | 94 | i, maxSqDist, sqDist, index; 95 | 96 | markers[first] = markers[last] = 1; 97 | 98 | while (last) { 99 | 100 | maxSqDist = 0; 101 | 102 | for (i = first + 1; i < last; i++) { 103 | sqDist = getSquareSegmentDistance(points[i], points[first], points[last]); 104 | 105 | if (sqDist > maxSqDist) { 106 | index = i; 107 | maxSqDist = sqDist; 108 | } 109 | } 110 | 111 | if (maxSqDist > sqTolerance) { 112 | markers[index] = 1; 113 | stack.push(first, index, index, last); 114 | } 115 | 116 | last = stack.pop(); 117 | first = stack.pop(); 118 | } 119 | 120 | for (i = 0; i < len; i++) { 121 | if (markers[i]) { 122 | newPoints.push(points[i]); 123 | } 124 | } 125 | 126 | return newPoints; 127 | } 128 | 129 | // both algorithms combined for awesome performance 130 | function simplify(points, tolerance, highestQuality) { 131 | 132 | var sqTolerance = tolerance !== undefined ? tolerance * tolerance : 1; 133 | 134 | points = highestQuality ? points : simplifyRadialDistance(points, sqTolerance); 135 | points = simplifyDouglasPeucker(points, sqTolerance); 136 | 137 | return points; 138 | } 139 | 140 | // export as a Node module, an AMD module or a global browser variable 141 | if (typeof module !== 'undefined') { 142 | module.exports = simplify; 143 | 144 | } else if (typeof define === 'function' && define.amd) { 145 | define(function() { 146 | return simplify; 147 | }); 148 | 149 | } else { 150 | window.simplify = simplify; 151 | } 152 | 153 | })(); 154 | -------------------------------------------------------------------------------- /src/public/assets/js/z-worker.js: -------------------------------------------------------------------------------- 1 | /* jshint worker:true */ 2 | (function main(global) { 3 | "use strict"; 4 | 5 | if (global.zWorkerInitialized) 6 | throw new Error('z-worker.js should be run only once'); 7 | global.zWorkerInitialized = true; 8 | 9 | addEventListener("message", function(event) { 10 | var message = event.data, type = message.type, sn = message.sn; 11 | var handler = handlers[type]; 12 | if (handler) { 13 | try { 14 | handler(message); 15 | } catch (e) { 16 | onError(type, sn, e); 17 | } 18 | } 19 | //for debug 20 | //postMessage({type: 'echo', originalType: type, sn: sn}); 21 | }); 22 | 23 | var handlers = { 24 | importScripts: doImportScripts, 25 | newTask: newTask, 26 | append: processData, 27 | flush: processData, 28 | }; 29 | 30 | // deflater/inflater tasks indexed by serial numbers 31 | var tasks = {}; 32 | 33 | function doImportScripts(msg) { 34 | if (msg.scripts && msg.scripts.length > 0) 35 | importScripts.apply(undefined, msg.scripts); 36 | postMessage({type: 'importScripts'}); 37 | } 38 | 39 | function newTask(msg) { 40 | var CodecClass = global[msg.codecClass]; 41 | var sn = msg.sn; 42 | if (tasks[sn]) 43 | throw Error('duplicated sn'); 44 | tasks[sn] = { 45 | codec: new CodecClass(msg.options), 46 | crcInput: msg.crcType === 'input', 47 | crcOutput: msg.crcType === 'output', 48 | crc: new Crc32(), 49 | }; 50 | postMessage({type: 'newTask', sn: sn}); 51 | } 52 | 53 | // performance may not be supported 54 | var now = global.performance ? global.performance.now.bind(global.performance) : Date.now; 55 | 56 | function processData(msg) { 57 | var sn = msg.sn, type = msg.type, input = msg.data; 58 | var task = tasks[sn]; 59 | // allow creating codec on first append 60 | if (!task && msg.codecClass) { 61 | newTask(msg); 62 | task = tasks[sn]; 63 | } 64 | var isAppend = type === 'append'; 65 | var start = now(); 66 | var output; 67 | if (isAppend) { 68 | try { 69 | output = task.codec.append(input, function onprogress(loaded) { 70 | postMessage({type: 'progress', sn: sn, loaded: loaded}); 71 | }); 72 | } catch (e) { 73 | delete tasks[sn]; 74 | throw e; 75 | } 76 | } else { 77 | delete tasks[sn]; 78 | output = task.codec.flush(); 79 | } 80 | var codecTime = now() - start; 81 | 82 | start = now(); 83 | if (input && task.crcInput) 84 | task.crc.append(input); 85 | if (output && task.crcOutput) 86 | task.crc.append(output); 87 | var crcTime = now() - start; 88 | 89 | var rmsg = {type: type, sn: sn, codecTime: codecTime, crcTime: crcTime}; 90 | var transferables = []; 91 | if (output) { 92 | rmsg.data = output; 93 | transferables.push(output.buffer); 94 | } 95 | if (!isAppend && (task.crcInput || task.crcOutput)) 96 | rmsg.crc = task.crc.get(); 97 | 98 | // posting a message with transferables will fail on IE10 99 | try { 100 | postMessage(rmsg, transferables); 101 | } catch(ex) { 102 | postMessage(rmsg); // retry without transferables 103 | } 104 | } 105 | 106 | function onError(type, sn, e) { 107 | var msg = { 108 | type: type, 109 | sn: sn, 110 | error: formatError(e) 111 | }; 112 | postMessage(msg); 113 | } 114 | 115 | function formatError(e) { 116 | return { message: e.message, stack: e.stack }; 117 | } 118 | 119 | // Crc32 code copied from file zip.js 120 | function Crc32() { 121 | this.crc = -1; 122 | } 123 | Crc32.prototype.append = function append(data) { 124 | var crc = this.crc | 0, table = this.table; 125 | for (var offset = 0, len = data.length | 0; offset < len; offset++) 126 | crc = (crc >>> 8) ^ table[(crc ^ data[offset]) & 0xFF]; 127 | this.crc = crc; 128 | }; 129 | Crc32.prototype.get = function get() { 130 | return ~this.crc; 131 | }; 132 | Crc32.prototype.table = (function() { 133 | var i, j, t, table = []; // Uint32Array is actually slower than [] 134 | for (i = 0; i < 256; i++) { 135 | t = i; 136 | for (j = 0; j < 8; j++) 137 | if (t & 1) 138 | t = (t >>> 1) ^ 0xEDB88320; 139 | else 140 | t = t >>> 1; 141 | table[i] = t; 142 | } 143 | return table; 144 | })(); 145 | 146 | // "no-op" codec 147 | function NOOP() {} 148 | global.NOOP = NOOP; 149 | NOOP.prototype.append = function append(bytes, onprogress) { 150 | return bytes; 151 | }; 152 | NOOP.prototype.flush = function flush() {}; 153 | })(this); 154 | -------------------------------------------------------------------------------- /src/public/assets/models/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CreativeCodingLab/Inviso/4bd24c00ceafbfb91b78433051a1d18b421b4d6a/src/public/assets/models/github.png -------------------------------------------------------------------------------- /src/public/assets/models/load.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 2007-09-12 00:15ZCanvas 1Layer 1 4 | -------------------------------------------------------------------------------- /src/public/assets/models/mute.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml 61 | 62 | 68 | 74 | -------------------------------------------------------------------------------- /src/public/assets/models/save.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Svg Vector Icons : http://www.onlinewebfonts.com/icon 6 | 7 | -------------------------------------------------------------------------------- /src/public/assets/models/unmute.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml 23 | 26 | 29 | 30 | -------------------------------------------------------------------------------- /src/public/assets/sounds/ambience1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CreativeCodingLab/Inviso/4bd24c00ceafbfb91b78433051a1d18b421b4d6a/src/public/assets/sounds/ambience1.wav -------------------------------------------------------------------------------- /src/public/assets/sounds/ambience2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CreativeCodingLab/Inviso/4bd24c00ceafbfb91b78433051a1d18b421b4d6a/src/public/assets/sounds/ambience2.wav -------------------------------------------------------------------------------- /src/public/assets/sounds/beat1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CreativeCodingLab/Inviso/4bd24c00ceafbfb91b78433051a1d18b421b4d6a/src/public/assets/sounds/beat1.wav -------------------------------------------------------------------------------- /src/public/assets/sounds/beat2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CreativeCodingLab/Inviso/4bd24c00ceafbfb91b78433051a1d18b421b4d6a/src/public/assets/sounds/beat2.wav -------------------------------------------------------------------------------- /src/public/assets/sounds/chirps.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CreativeCodingLab/Inviso/4bd24c00ceafbfb91b78433051a1d18b421b4d6a/src/public/assets/sounds/chirps.wav -------------------------------------------------------------------------------- /src/public/assets/sounds/percussion.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CreativeCodingLab/Inviso/4bd24c00ceafbfb91b78433051a1d18b421b4d6a/src/public/assets/sounds/percussion.wav -------------------------------------------------------------------------------- /src/public/assets/sounds/speech1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CreativeCodingLab/Inviso/4bd24c00ceafbfb91b78433051a1d18b421b4d6a/src/public/assets/sounds/speech1.wav -------------------------------------------------------------------------------- /src/public/assets/sounds/speech2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CreativeCodingLab/Inviso/4bd24c00ceafbfb91b78433051a1d18b421b4d6a/src/public/assets/sounds/speech2.wav -------------------------------------------------------------------------------- /src/public/assets/sounds/synth1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CreativeCodingLab/Inviso/4bd24c00ceafbfb91b78433051a1d18b421b4d6a/src/public/assets/sounds/synth1.wav -------------------------------------------------------------------------------- /src/public/assets/sounds/synth2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CreativeCodingLab/Inviso/4bd24c00ceafbfb91b78433051a1d18b421b4d6a/src/public/assets/sounds/synth2.wav -------------------------------------------------------------------------------- /src/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Inviso 6 | 7 | 8 | 9 | 10 |
11 |
Loading...
12 | 13 |
14 |
15 | 16 |
17 |
18 | 19 |
20 |
21 | 22 |
23 | 24 | 25 |
26 | 27 |
28 |
29 | 30 | 35 | 36 |
37 |

After clicking the add button, click anywhere on the screen to

38 |

create a sound object, or click & drag to draw a sound zone.

39 |
40 | 41 |
Aerial view
42 | 43 |
44 |

Click & drag anywhere on the screen to rotate the

45 |

view; right-click & drag to pan. When Altitude View

46 |

is active, you can change the height of a sound object

47 |

or a trajectory point.

48 |
49 | 50 |
51 |

Use W, A, S, D keys to move the head inside the scene.

52 |
53 |
54 |
55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // Global imports 2 | const webpack = require('webpack'); 3 | const path = require('path'); 4 | 5 | // Paths 6 | const entry = './src/js/app.js'; 7 | const includePath = path.join(__dirname, 'src/js'); 8 | const nodeModulesPath = path.join(__dirname, 'node_modules'); 9 | let outputPath = path.join(__dirname, 'src/public/assets/js'); 10 | 11 | // Environment 12 | const PROD = JSON.parse(process.env.NODE_ENV || 0); 13 | 14 | // Dev environment 15 | let env = 'dev'; 16 | let devtool = 'eval'; 17 | let debug = true; 18 | 19 | const time = Date.now(); 20 | const plugins = [ 21 | new webpack.NoErrorsPlugin(), 22 | new webpack.DefinePlugin({ 23 | __ENV__: JSON.stringify(env), 24 | ___BUILD_TIME___: time, 25 | }), 26 | ]; 27 | 28 | // Production environment 29 | if (PROD) { 30 | env = 'prod'; 31 | devtool = 'hidden-source-map'; 32 | debug = false; 33 | outputPath = path.join(__dirname, '/build/public/assets/js'); 34 | 35 | const uglifyOptions = { 36 | sourceMap: false, 37 | mangle: true, 38 | compress: { 39 | drop_console: true, 40 | }, 41 | output: { 42 | comments: false, 43 | }, 44 | }; 45 | plugins.push(new webpack.optimize.UglifyJsPlugin(uglifyOptions)); 46 | } 47 | 48 | console.log('Webpack build - ENV: ' + env + ' V: ' + time); 49 | console.log(' - outputPath ', outputPath); 50 | console.log(' - includePath ', includePath); 51 | console.log(' - nodeModulesPath ', nodeModulesPath); 52 | 53 | module.exports = { 54 | stats: { 55 | colors: true, 56 | }, 57 | debug, 58 | devtool, 59 | devServer: { 60 | contentBase: 'src/public', 61 | }, 62 | entry: [entry], 63 | output: { 64 | path: outputPath, 65 | publicPath: 'assets/js', 66 | filename: 'app.js', 67 | }, 68 | module: { 69 | loaders: [ 70 | { 71 | test: /\.scss$/, 72 | loaders: ['style-loader', 'css-loader', 'sass-loader'], 73 | }, 74 | { 75 | test: /\.js?$/, 76 | loader: 'babel-loader', 77 | query: { 78 | retainLines: true, 79 | presets: ['es2015'], 80 | }, 81 | include: [includePath, nodeModulesPath], 82 | }, 83 | ], 84 | }, 85 | plugins, 86 | }; 87 | --------------------------------------------------------------------------------